| @@ -1,138 +0,0 @@ | |||||
| # Code Grade & Production Readiness Report (Updated) | |||||
| ## Overall grade: **7.0 / 10** | |||||
| The codebase has been refactored into a clear app/service layout with thin `cmd` entrypoints, shared `internal/pkg` libraries, health/readiness endpoints, structured middleware, and addressed reliability/security items. It is suitable for development and staging; production use still requires CORS restriction, optional metrics/tracing, and (if desired) request validation and OpenAPI. | |||||
| --- | |||||
| ## 1. What’s working well | |||||
| | Area | Notes | | |||||
| |------|--------| | |||||
| | **Structure** | `cmd/<service>/main.go` is thin (~25 lines); `internal/app/*` holds per-service composition; `internal/pkg` has api (response, middleware, handler), location, bridge, decoder, config, kafkaclient, logger, model, controller, service, database, apiclient, appcontext. | | |||||
| | **Concurrency** | Channels, `sync.WaitGroup`, and `AppState` with RWMutex; event loops live in app layer, not in main. | | |||||
| | **Shutdown** | `signal.NotifyContext` + app `Run`/`Shutdown`; Kafka and MQTT cleanup in app. | | |||||
| | **Kafka** | `KafkaManager`, generic `Consume[T]`, graceful close. | | |||||
| | **Observability** | `/health` and `/ready` (DB ping); middleware: logging, recovery, request ID, CORS; logging to file with fallback to stderr if file open fails. | | |||||
| | **Reliability** | No panics in library code for logger (fallback to stderr); MQTT connect returns error; server init returns error; `WriteMessages` errors checked in parser service and settings controller. | | |||||
| | **Security** | TLS skip verify is configurable via `TLS_INSECURE_SKIP_VERIFY` (default false). | | |||||
| | **Testing** | Unit tests for appcontext, utils, model, controller, service, config; integration tests for bridge/decoder. | | |||||
| | **Dependencies** | Modern stack (slog, segmentio/kafka-go, gorilla/mux, gorm). | | |||||
| --- | |||||
| ## 2. Fixes applied since last report | |||||
| ### 2.1 Startup and library behavior | |||||
| - **Bridge:** MQTT connect failure no longer panics; `internal/pkg/bridge/mqtt.go` returns error from `NewMQTTClient`, `cmd/bridge/main.go` exits with `log.Fatalf` on error. | |||||
| - **Server:** DB and config init live in `internal/app/server`; `New`/`Init` return errors; `cmd/server/main.go` uses `log.Fatalf` on error (no panic in library). | |||||
| - **Logger:** `CreateLogger` no longer uses `log.Fatalf`; on log file open failure it returns a logger that writes only to stderr and a no-op cleanup. | |||||
| ### 2.2 Ignored errors | |||||
| - **parser_service.go:** `writer.WriteMessages(ctx, msg)` return value is checked and propagated. | |||||
| - **settings_controller.go:** `writer.WriteMessages` error is checked; on failure returns 500 and logs; response sets `Content-Type: application/json`. | |||||
| - **database:** Unused global `var DB *gorm.DB` removed. | |||||
| ### 2.3 Security and configuration | |||||
| - **TLS:** `config.Config` has `TLSInsecureSkipVerify bool` (env `TLS_INSECURE_SKIP_VERIFY`, default false). Used in `apiclient.UpdateDB` and in location inference (`NewDefaultInferencer(cfg.TLSInsecureSkipVerify)`). | |||||
| - **CORS:** Not changed (origin policy left to operator; middleware supports configurable origins). | |||||
| ### 2.4 Observability | |||||
| - **Health/readiness:** Server exposes `/health` (liveness) and `/ready` (DB ping) via `internal/pkg/api/handler/health.go`. | |||||
| - **Middleware:** Recovery (panic → 500), logging (method, path, status, duration), request ID (`X-Request-ID`), CORS. | |||||
| ### 2.5 Code quality | |||||
| - **Bridge:** MQTT topic parsing uses `strings.SplitN(topic, "/", 2)` to avoid panic; CSV branch validates and logs (no writer usage yet). | |||||
| - **Location:** Magic numbers moved to named constants in `internal/pkg/location/filter.go` (e.g. `SeenWeight`, `RSSIWeight`, `DefaultDistance`). | |||||
| - **Duplication:** Bootstrap removed; each service uses `internal/app/<service>` for init, run, and shutdown. | |||||
| --- | |||||
| ## 3. Remaining / known limitations | |||||
| ### 3.1 Config and env | |||||
| - **`getEnvPanic`** in `config` still panics on missing required env. To avoid panics in library, consider a `LoadServerSafe` (or similar) that returns `(*Config, error)` and use it only from `main` with explicit exit. Not changed in this pass. | |||||
| ### 3.2 Security | |||||
| - **CORS:** Defaults remain permissive (e.g. `*`). Restrict to known frontend origins when deploying (e.g. via env or config). | |||||
| - **Secrets:** Still loaded from env only; ensure no secrets in logs; consider a secret manager for production. | |||||
| ### 3.3 API and validation | |||||
| - No OpenAPI/Swagger; no formal request/response contracts. | |||||
| - Many handlers still use `http.Error` or `w.Write` without a single response helper; `api/response` exists for new/consistent endpoints. | |||||
| - No request body validation (e.g. go-playground/validator); no idempotency keys. | |||||
| ### 3.4 Resilience and operations | |||||
| - Kafka consumer: on `ReadMessage`/unmarshal error, logs and continues; no dead-letter or backoff yet. | |||||
| - DB: no documented pool tuning; readiness only checks DB ping. | |||||
| - No metrics (Prometheus/OpenTelemetry). No distributed tracing. | |||||
| --- | |||||
| ## 4. Grade breakdown (updated) | |||||
| | Criterion | Score | Comment | | |||||
| |---------------------|-------|--------| | |||||
| | Architecture | 8/10 | Clear app layer, thin main, pkg separation; handlers still take concrete DB/writer (can be abstracted later). | | |||||
| | Reliability | 7/10 | No panics in logger/bridge init; WriteMessages errors handled; health/ready; logger fallback. | | |||||
| | Security | 6/10 | TLS skip verify configurable (default off); CORS still broad; secrets in env. | | |||||
| | Observability | 7/10 | Health/ready, request logging, request ID, recovery; no metrics/tracing. | | |||||
| | API design | 6/10 | Response helpers and middleware in place; many handlers still ad-hoc; no spec/validation. | | |||||
| | Testing | 6/10 | Good unit coverage; more integration/E2E would help. | | |||||
| | Code quality | 8/10 | Clear structure, constants for magic numbers, dead code removed, duplication reduced. | | |||||
| | Production readiness | 6/10 | Health/ready and error handling in place; CORS, metrics, and validation still to do. | | |||||
| **Average ≈ 6.75; grade 7.0/10** – Refactor and applied fixes significantly improve structure, reliability, and observability; remaining work is mostly CORS, validation, and metrics/tracing. | |||||
| --- | |||||
| ## 5. Checklist (updated) | |||||
| ### 5.1 Reliability | |||||
| - [x] Remove panics / `log.Fatalf` from library where possible (logger fallback; bridge returns error). | |||||
| - [x] Check and handle `WriteMessages` in parser service and settings controller. | |||||
| - [x] Add `/health` and `/ready` on server. | |||||
| - [ ] Document or add Kafka consumer retry/backoff and dead-letter if needed. | |||||
| - [x] Make TLS skip verify configurable; default false. | |||||
| ### 5.2 Observability | |||||
| - [x] Structured logging and request ID middleware. | |||||
| - [ ] Add metrics (e.g. Prometheus) and optional tracing. | |||||
| ### 5.3 API and validation | |||||
| - [ ] OpenAPI spec and validation. | |||||
| - [ ] Consistent use of `api/response` and JSON error body across handlers. | |||||
| - [ ] Restrict CORS to specific origins (operator-defined). | |||||
| ### 5.4 Operations | |||||
| - [ ] Document env vars and deployment topology. | |||||
| - [ ] Configurable timeouts; rate limiting if required. | |||||
| ### 5.5 Code and structure | |||||
| - [x] Bridge topic parsing and CSV branch behavior clarified. | |||||
| - [x] Unused `database.DB` global removed. | |||||
| - [x] Location magic numbers moved to constants. | |||||
| - [x] App layer and api/middleware/response in place. | |||||
| --- | |||||
| ## 6. Summary | |||||
| - **Grade: 7.0/10** – Refactor and targeted fixes improve structure, reliability, and observability. Server has health/ready, middleware, and no panics in logger/bridge init; TLS skip verify is configurable; WriteMessages and logger errors are handled. | |||||
| - **Still to do for production:** Restrict CORS, add metrics (and optionally tracing), validate requests and adopt consistent API responses, and document operations. Config loading can be made panic-free by adding safe loaders that return errors. | |||||
| - **Not changed by design:** CORS policy left for operator to configure (e.g. via env or config). | |||||
| @@ -0,0 +1,210 @@ | |||||
| # Production Readiness Report — Microservices (server, bridge, decoder, location) | |||||
| **Scope:** `cmd/server`, `cmd/bridge`, `cmd/decoder`, `cmd/location` and all packages they import. | |||||
| **Date:** 2025-03-05. | |||||
| --- | |||||
| ## Overall grade: **5.5 / 10** | |||||
| The codebase has a **solid structure** and **consistent patterns** across the four services, but **security**, **reliability**, and **operational hardening** are not yet at production level. With the changes suggested below, it can be brought to a 7–8/10 for a production deployment. | |||||
| --- | |||||
| ## 1. Summary by dimension | |||||
| | Dimension | Grade | Notes | | |||||
| | --------------------------- | ----- | ------------------------------------------------------------------------------------------------- | | |||||
| | **Structure & readability** | 7/10 | Clear app lifecycle (New/Run/Shutdown), good package layout, some naming/duplication issues. | | |||||
| | **Reliability** | 5/10 | Graceful shutdown and Kafka cleanup are good; missing retries, commit semantics, and DB/timeouts. | | |||||
| | **Security** | 3/10 | No API auth, TLS/DB and client TLS weakened, CORS permissive, no rate limiting. | | |||||
| | **Observability** | 6/10 | slog, request ID, logging middleware, health/ready; no metrics/tracing. | | |||||
| | **Correctness** | 6/10 | Path vs body ID bugs in update endpoints, `context.Background()` in hot paths. | | |||||
| --- | |||||
| ## 2. What’s in good shape | |||||
| - **Unified app pattern:** All four services use the same lifecycle: `New(cfg)` → optional `Init(ctx)` (server only) → `Run(ctx)` → `Shutdown()`, with `signal.NotifyContext` for graceful shutdown. | |||||
| - **Graceful shutdown:** HTTP server shutdown, Kafka readers/writers and MQTT disconnect are explicitly closed; `sync.WaitGroup` used for consumer goroutines. | |||||
| - **Structured logging:** `slog` with JSON handler and file + stderr; request logging with method, path, status, duration, bytes. | |||||
| - **HTTP middleware:** Recovery (panic → 500), request ID, CORS, logging applied in a clear chain. | |||||
| - **Health endpoints:** `/health` (liveness) and `/ready` (DB ping) for the server. | |||||
| - **Kafka usage:** Centralized `KafkaManager` with RWMutex, separate readers/writers, group IDs per service. | |||||
| - **Shared state:** `AppState` in `common/appcontext` is thread-safe (RWMutex) and used consistently. | |||||
| - **Config:** Env-based config with service-specific loaders (`LoadServer`, `LoadBridge`, etc.) and `getEnvPanic` for required vars. | |||||
| - **API responses:** Centralized `response.JSON`, `Error`, `BadRequest`, `InternalError`, `NotFound` with consistent JSON shape. | |||||
| - **OpenAPI:** Routes reference `api/openapi.yaml` (OpenAPI 3.0), which helps readability and contract clarity. | |||||
| --- | |||||
| ## 3. Critical issues | |||||
| ### 3.1 Security | |||||
| - **No authentication or authorization on the HTTP API** | |||||
| All server routes (`/reslevis/*`, `/configs/beacons`, etc.) are unauthenticated. Anyone who can reach the server can read/update/delete gateways, zones, trackers, parser configs, settings, alerts, and tracks. | |||||
| - **Database connection uses `sslmode=disable`** | |||||
| In `internal/pkg/database/database.go`, DSN is built with `sslmode=disable`. In production, DB connections should use TLS and `sslmode=verify-full` (or equivalent) with CA verification. | |||||
| - **TLS verification disabled for outbound HTTP** | |||||
| - `internal/pkg/apiclient/updatedb.go`: `TLSClientConfig: &tls.Config{InsecureSkipVerify: true}`. | |||||
| - `internal/pkg/location/inference.go`: same, and **`NewDefaultInferencer(skipTLSVerify bool)` ignores the parameter** and always uses `InsecureSkipVerify: true`. | |||||
| - **CORS defaults to `*`** | |||||
| In `internal/pkg/api/middleware/cors.go`, when `origins` is nil/empty, `origins = []string{"*"}`. Production should restrict origins to known front-end origins. | |||||
| - **Logger file mode `0666`** | |||||
| In `internal/pkg/logger/logger.go`, `os.OpenFile(..., 0666)` makes the log file world-readable and -writable. Prefer `0600` or `0640`. | |||||
| - **No rate limiting or request body size limits** | |||||
| No protection against abuse or large-body DoS; consider middleware for max body size and rate limiting (per IP or per key). | |||||
| **Recommendations:** | |||||
| - Add authentication/authorization middleware (e.g. JWT or API key validation) for all non-health API routes; keep `/health` and optionally `/ready` public. | |||||
| - Make DB TLS configurable via env (e.g. `DB_SSLMODE`, `DB_SSLROOTCERT`) and use `sslmode=verify-full` in production. | |||||
| - Use `cfg.TLSInsecureSkipVerify` (or equivalent) for all outbound HTTP clients; fix `NewDefaultInferencer` to respect the parameter. | |||||
| - Configure CORS with explicit allowed origins (and optionally credentials) from config. | |||||
| - Set log file mode to `0600` (or `0640` if a group needs read). | |||||
| - Add middleware to limit request body size (e.g. `http.MaxBytesReader`) and consider rate limiting for API routes. | |||||
| --- | |||||
| ### 3.2 Reliability | |||||
| - **Kafka consumer: decode errors and commit semantics** | |||||
| In `internal/pkg/kafkaclient/consumer.go`, when `json.Unmarshal` fails, the code logs and `continue`s without committing. Depending on reader config, this can cause repeated redelivery of bad messages or ambiguous semantics. Production should either skip and commit, or send to a dead-letter path and commit. | |||||
| Answer: because readers are using consumer groups messages are auto commited, meaning bad unmarshal still commits as the message was technically read | |||||
| - **No retries on Kafka produce** | |||||
| Event loops (server, bridge, decoder, location) call `WriteMessages` once; transient Kafka errors are not retried. Consider retry with backoff (and optional circuit breaker) for critical topics. | |||||
| Answer: the Writer object is already holding the default configuration for timeout, backoff and retries, but I still added some extra configurations | |||||
| - **Database: no explicit pool or timeouts** | |||||
| `database.Connect` uses GORM defaults. For production, set `MaxOpenConns`, `MaxIdleConns`, and connection/timeout settings (e.g. `SetConnMaxLifetime`) on the underlying `*sql.DB`. | |||||
| - **UpdateDB and Init: failures only logged** | |||||
| In `internal/app/server/app.go`, `apiclient.UpdateDB` errors are only logged; Init continues. Consider failing Init (or marking “degraded”) if sync is required for correct operation, or add retries/backoff. | |||||
| - **Use of `context.Background()` in async paths** | |||||
| e.g. `internal/pkg/bridge/handler.go` and `internal/pkg/decoder/process.go` use `context.Background()` for Kafka writes. Prefer passing the request/event context (or a derived timeout) so shutdown and timeouts propagate. | |||||
| **Recommendations:** | |||||
| - Define a clear policy for Kafka consumer errors (skip+commit vs DLQ); avoid silent continue without commit unless intended. | |||||
| - Add retry (with backoff) for Kafka produce in critical paths; consider a small wrapper or helper. | |||||
| - Configure DB pool and timeouts in `database.Connect` (and optionally make them configurable via config). | |||||
| - Decide whether UpdateDB is mandatory for startup; if yes, fail Init on error or retry; if no, document and consider a “degraded” readiness state. | |||||
| - Pass context from the caller (or a timeout context) into Kafka write calls instead of `context.Background()`. | |||||
| --- | |||||
| ### 3.3 Correctness and consistency | |||||
| - **Update endpoints: path `id` vs body `id`** | |||||
| - **GatewayUpdateController** (`internal/pkg/controller/gateways_controller.go`): Uses `mux.Vars(r)["id"]` only to check existence with `First(..., "id = ?", id)`, then decodes body into `gateway` and calls `Save(&gateway)`. The updated record is identified by `gateway.ID` from the body, not the path. A client can send a different ID in the body and update another resource. | |||||
| - **ZoneUpdateController**: Route is `updateZone` (no `{id}` in path); uses `zone.ID` from body only. If the API contract expects path-based ID, this is inconsistent. | |||||
| Recommendation: For update-by-id, use the path parameter as the single source of truth: load by path `id`, decode body into a DTO or partial struct, then update only allowed fields for that id (e.g. selective updates or merge then update by path id). | |||||
| - **TrackerUpdateController** | |||||
| Uses body `tracker.ID` for lookup and save; route has no `{id}` in path. If other update endpoints use path `{id}`, align behavior and documentation. | |||||
| **Recommendations:** | |||||
| - Standardize update semantics: either path `{id}` only (body has no id or it must match path) or document “body id is canonical” and ensure no IDOR. | |||||
| - Prefer path-based resource identification for updates/deletes and bind body to allowed fields only. | |||||
| --- | |||||
| ## 4. Minor issues and improvements | |||||
| - **Logging:** Replace `fmt.Println` in `internal/pkg/apiclient/auth.go` and any `internal/pkg/apiclient/updatedb.go` / inference paths with `slog` (or structured logger) so logs are consistent and configurable. | |||||
| - **Token lifecycle:** `DefaultInferencer` caches token in a struct field with no expiry or refresh; token may be used after expiry. Use token expiry from auth response and refresh when needed. | |||||
| - **BeaconLookup naming:** In `appcontext`, `BeaconExists(id string)` is used with MAC (e.g. in bridge handler). Rename parameter to `mac` (or the method to `LookupIDByMAC`) to avoid confusion. | |||||
| - **Bridge/location/decoder:** No `/health` or `/ready` endpoints. For orchestration (e.g. Kubernetes), consider a small HTTP server or at least a process-level health check so the platform can restart unhealthy instances. | |||||
| - **Dependencies:** `go.mod` is clear; consider auditing indirect deps and keeping them updated (e.g. `go list -m -u all` and Dependabot/Renovate). | |||||
| --- | |||||
| ## 5. Propositions (prioritized) | |||||
| ### P0 — Before production | |||||
| 1. **Add API authentication/authorization** | |||||
| Protect all non-health routes (e.g. JWT or API key middleware); document required claims/scopes if using JWT. | |||||
| 2. **Enable and verify DB TLS** | |||||
| Make `sslmode` (and optional root cert) configurable; use `verify-full` (or equivalent) in production. | |||||
| 3. **Respect TLS config for outbound HTTP** | |||||
| Use config (e.g. `TLSInsecureSkipVerify`) for apiclient and location inferencer; fix `NewDefaultInferencer(skipTLSVerify bool)` to use the parameter. | |||||
| 4. **Fix update controllers** | |||||
| Use path `id` as source of truth for update (and optionally delete); ensure body cannot override resource id in an unsafe way. | |||||
| 5. **Tighten CORS and log file permissions** | |||||
| Explicit allowed origins from config; set log file mode to `0600` (or `0640`). | |||||
| ### P1 — High | |||||
| 6. **Kafka consumer error policy** | |||||
| Define and implement skip+commit or DLQ for bad messages; avoid infinite redelivery of poison messages. | |||||
| 7. **DB connection pool and timeouts** | |||||
| Set `MaxOpenConns`, `MaxIdleConns`, `ConnMaxLifetime`, and timeouts in `database.Connect`. | |||||
| 8. **Request body size limit** | |||||
| Middleware (e.g. `http.MaxBytesReader`) for API routes to prevent large-body DoS. | |||||
| 9. **Replace fmt.Println with slog** | |||||
| In apiclient and any remaining places; ensure response body is closed after read (e.g. `defer res.Body.Close()` in auth/data clients if not already). | |||||
| ### P2 — Medium | |||||
| 10. **Retries for Kafka produce** | |||||
| Retry with backoff (and optionally circuit breaker) for critical `WriteMessages` calls. | |||||
| 11. **Context propagation** | |||||
| Pass request/event context (or bounded context) into Kafka writes instead of `context.Background()`. | |||||
| 12. **Token refresh in Inferencer** | |||||
| Use expiry from auth response and refresh token before inference calls. | |||||
| 13. **Health for bridge/decoder/location** | |||||
| Add minimal health/readiness (HTTP or signal file) for orchestration and load balancers. | |||||
| ### P3 — Nice to have | |||||
| 14. **Metrics and tracing** | |||||
| Add metrics (e.g. request duration, Kafka lag, error counts) and optional distributed tracing (e.g. OTel). | |||||
| 15. **Rate limiting** | |||||
| Per-IP or per-token rate limiting on API routes. | |||||
| 16. **Structured validation** | |||||
| Use a validator (e.g. go-playground/validator) for request bodies and path params (IDs, limits). | |||||
| 17. **Documentation** | |||||
| Short runbooks for deploy, config env vars, and dependency on Kafka/DB/MQTT and external auth/API. | |||||
| --- | |||||
| ## 6. Files reviewed (representative) | |||||
| - **Entrypoints:** `cmd/server/main.go`, `cmd/bridge/main.go`, `cmd/decoder/main.go`, `cmd/location/main.go` | |||||
| - **Apps:** `internal/app/server/*`, `internal/app/bridge/app.go`, `internal/app/decoder/app.go`, `internal/app/location/app.go` | |||||
| - **Config:** `internal/pkg/config/config.go` | |||||
| - **Infra:** `internal/pkg/database/database.go`, `internal/pkg/kafkaclient/manager.go`, `internal/pkg/kafkaclient/consumer.go`, `internal/pkg/logger/logger.go` | |||||
| - **API:** `internal/pkg/api/handler/health.go`, `internal/pkg/api/middleware/*`, `internal/pkg/api/response/response.go`, `internal/app/server/routes.go` | |||||
| - **Controllers:** `internal/pkg/controller/*.go` (gateways, trackers, zone, parser, settings, alerts, tracks, trackerzones) | |||||
| - **Services:** `internal/pkg/service/beacon_service.go`, `internal/pkg/apiclient/*.go`, `internal/pkg/bridge/mqtt.go`, `internal/pkg/bridge/handler.go`, `internal/pkg/decoder/process.go`, `internal/pkg/location/inference.go`, `internal/pkg/common/appcontext/context.go` | |||||
| --- | |||||
| ## 7. Conclusion | |||||
| The microservices are **well-structured and readable**, with **consistent lifecycle and shutdown**. The main gaps are **security (no API auth, weak TLS usage)** and **reliability (Kafka and DB tuning, retries, context usage)**. Addressing the **P0 and P1** items above would bring the system much closer to production grade (around **7–8/10**); adding **P2/P3** would further improve operability and resilience. | |||||
| @@ -0,0 +1,16 @@ | |||||
| version: "2" | |||||
| services: | |||||
| postgres: | |||||
| image: postgres:18 | |||||
| container_name: postgres | |||||
| restart: always | |||||
| ports: | |||||
| - "127.0.0.1:5433:5432" | |||||
| env_file: | |||||
| - ./env/db.env | |||||
| healthcheck: | |||||
| test: ["CMD-SHELL", "pg_isready -U postgres"] | |||||
| interval: 5s | |||||
| timeout: 5s | |||||
| retries: 5 | |||||
| start_period: 30s | |||||
| @@ -1,7 +1,7 @@ | |||||
| version: "2" | version: "2" | ||||
| services: | services: | ||||
| db: | db: | ||||
| image: postgres | |||||
| image: postgres:18 | |||||
| container_name: db | container_name: db | ||||
| restart: always | restart: always | ||||
| ports: | ports: | ||||
| @@ -14,6 +14,8 @@ services: | |||||
| timeout: 5s | timeout: 5s | ||||
| retries: 5 | retries: 5 | ||||
| start_period: 30s | start_period: 30s | ||||
| volumes: | |||||
| - pgdata:/var/postgresql/data | |||||
| kafdrop: | kafdrop: | ||||
| image: obsidiandynamics/kafdrop | image: obsidiandynamics/kafdrop | ||||
| @@ -39,6 +41,8 @@ services: | |||||
| timeout: 5s | timeout: 5s | ||||
| retries: 10 | retries: 10 | ||||
| start_period: 20s | start_period: 20s | ||||
| volumes: | |||||
| - kafkadata:/var/lib/kafka/data | |||||
| kafka-init: | kafka-init: | ||||
| image: apache/kafka:3.9.0 | image: apache/kafka:3.9.0 | ||||
| @@ -73,7 +77,7 @@ services: | |||||
| restart: always | restart: always | ||||
| volumes: | volumes: | ||||
| - ../:/app | - ../:/app | ||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/decoder ./cmd/decoder" --build.bin "/tmp/decoder" | |||||
| command: air --build.cmd "go build -buildvcs=false -o ./decoder ./cmd/decoder" --build.bin "./decoder" | |||||
| presense-server: | presense-server: | ||||
| build: | build: | ||||
| @@ -95,7 +99,7 @@ services: | |||||
| restart: always | restart: always | ||||
| volumes: | volumes: | ||||
| - ../:/app | - ../:/app | ||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/server ./cmd/server" --build.bin "/tmp/server" | |||||
| command: air --build.cmd "go build -buildvcs=false -o ./server ./cmd/server" --build.bin "./server" | |||||
| presense-bridge: | presense-bridge: | ||||
| build: | build: | ||||
| @@ -113,7 +117,7 @@ services: | |||||
| restart: always | restart: always | ||||
| volumes: | volumes: | ||||
| - ../:/app | - ../:/app | ||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/bridge ./cmd/bridge" --build.bin "/tmp/bridge" | |||||
| command: air --build.cmd "go build -buildvcs=false -o ./bridge ./cmd/bridge" --build.bin "./bridge" | |||||
| presense-location: | presense-location: | ||||
| build: | build: | ||||
| @@ -131,7 +135,8 @@ services: | |||||
| restart: always | restart: always | ||||
| volumes: | volumes: | ||||
| - ../:/app | - ../:/app | ||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/location ./cmd/location" --build.bin "/tmp/location" | |||||
| command: air --build.cmd "go build -buildvcs=false -o ./location ./cmd/location" --build.bin "./location" | |||||
| volumes: | |||||
| pgdata: | |||||
| kafkadata: | |||||
| @@ -1,6 +1,6 @@ | |||||
| services: | services: | ||||
| db: | db: | ||||
| image: postgres | |||||
| image: postgres:18 | |||||
| container_name: db | container_name: db | ||||
| restart: always | restart: always | ||||
| ports: | ports: | ||||
| @@ -13,6 +13,8 @@ services: | |||||
| timeout: 5s | timeout: 5s | ||||
| retries: 5 | retries: 5 | ||||
| start_period: 30s | start_period: 30s | ||||
| volumes: | |||||
| - pgdata:/var/postgresql/data | |||||
| kafdrop: | kafdrop: | ||||
| image: obsidiandynamics/kafdrop | image: obsidiandynamics/kafdrop | ||||
| @@ -37,6 +39,8 @@ services: | |||||
| timeout: 5s | timeout: 5s | ||||
| retries: 10 | retries: 10 | ||||
| start_period: 20s | start_period: 20s | ||||
| volumes: | |||||
| - kafkadata:/var/lib/kafka/data | |||||
| kafka-init: | kafka-init: | ||||
| image: apache/kafka:3.9.0 | image: apache/kafka:3.9.0 | ||||
| @@ -108,4 +112,6 @@ services: | |||||
| restart: always | restart: always | ||||
| volumes: | |||||
| pgdata: | |||||
| kafkadata: | |||||
| @@ -3,6 +3,7 @@ package apiclient | |||||
| import ( | import ( | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | "fmt" | ||||
| "io" | |||||
| "net/http" | "net/http" | ||||
| "github.com/AFASystems/presence/internal/pkg/config" | "github.com/AFASystems/presence/internal/pkg/config" | ||||
| @@ -16,6 +17,13 @@ func GetTrackers(token string, client *http.Client, cfg *config.Config) ([]model | |||||
| return []model.Tracker{}, err | return []model.Tracker{}, err | ||||
| } | } | ||||
| bodyBytes, err := io.ReadAll(res.Body) | |||||
| if err != nil { | |||||
| fmt.Printf("error read body: %+v\n", err) | |||||
| return []model.Tracker{}, err | |||||
| } | |||||
| fmt.Printf("body: %s\n", string(bodyBytes)) | |||||
| var i []model.Tracker | var i []model.Tracker | ||||
| err = json.NewDecoder(res.Body).Decode(&i) | err = json.NewDecoder(res.Body).Decode(&i) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -74,7 +82,6 @@ func GetZones(token string, client *http.Client, cfg *config.Config) ([]model.Zo | |||||
| func InferPosition(token string, client *http.Client, cfg *config.Config) (model.PositionResponse, error) { | func InferPosition(token string, client *http.Client, cfg *config.Config) (model.PositionResponse, error) { | ||||
| url := fmt.Sprintf("%s/ble-ai/infer", cfg.APIBaseURL) | url := fmt.Sprintf("%s/ble-ai/infer", cfg.APIBaseURL) | ||||
| fmt.Printf("url: %s\n", url) | |||||
| req, err := http.NewRequest("GET", url, nil) | req, err := http.NewRequest("GET", url, nil) | ||||
| if err != nil { | if err != nil { | ||||
| fmt.Printf("error new request: %+v\n", err) | fmt.Printf("error new request: %+v\n", err) | ||||
| @@ -89,6 +96,12 @@ func InferPosition(token string, client *http.Client, cfg *config.Config) (model | |||||
| return model.PositionResponse{}, err | return model.PositionResponse{}, err | ||||
| } | } | ||||
| fmt.Printf("res.status: %s\n", res.Status) | |||||
| if res.StatusCode != 200 { | |||||
| fmt.Printf("error status code: %d\n", res.StatusCode) | |||||
| return model.PositionResponse{}, fmt.Errorf("status code: %d", res.StatusCode) | |||||
| } | |||||
| var i model.PositionResponse | var i model.PositionResponse | ||||
| err = json.NewDecoder(res.Body).Decode(&i) | err = json.NewDecoder(res.Body).Decode(&i) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -29,6 +29,7 @@ func UpdateDB(db *gorm.DB, ctx context.Context, cfg *config.Config, writer *kafk | |||||
| } | } | ||||
| if trackers, err := GetTrackers(token, client, cfg); err == nil { | if trackers, err := GetTrackers(token, client, cfg); err == nil { | ||||
| fmt.Printf("trackers: %+v\n", trackers) | |||||
| syncTable(db, trackers) | syncTable(db, trackers) | ||||
| if err := controller.SendKafkaMessage(writer, &model.ApiUpdate{Method: "DELETE", MAC: "all"}, ctx); err != nil { | if err := controller.SendKafkaMessage(writer, &model.ApiUpdate{Method: "DELETE", MAC: "all"}, ctx); err != nil { | ||||
| msg := fmt.Sprintf("Error in sending delete all from lookup message: %v", err) | msg := fmt.Sprintf("Error in sending delete all from lookup message: %v", err) | ||||
| @@ -14,6 +14,7 @@ func setHeader(req *http.Request, token string) { | |||||
| func getRequest(token, route string, client *http.Client, cfg *config.Config) (*http.Response, error) { | func getRequest(token, route string, client *http.Client, cfg *config.Config) (*http.Response, error) { | ||||
| url := fmt.Sprintf("%s/reslevis/%s", cfg.APIBaseURL, route) | url := fmt.Sprintf("%s/reslevis/%s", cfg.APIBaseURL, route) | ||||
| fmt.Printf("url: %s\n", url) | |||||
| req, err := http.NewRequest("GET", url, nil) | req, err := http.NewRequest("GET", url, nil) | ||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| @@ -7,15 +7,11 @@ import ( | |||||
| "strings" | "strings" | ||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| // RawBeaconWriter writes beacon advertisements to the rawbeacons topic. | |||||
| type RawBeaconWriter interface { | |||||
| WriteMessages(ctx context.Context, msgs ...kafka.Message) error | |||||
| } | |||||
| // BeaconLookup provides MAC->ID lookup (e.g. AppState). | // BeaconLookup provides MAC->ID lookup (e.g. AppState). | ||||
| type BeaconLookup interface { | type BeaconLookup interface { | ||||
| BeaconExists(mac string) (id string, ok bool) | BeaconExists(mac string) (id string, ok bool) | ||||
| @@ -24,7 +20,7 @@ type BeaconLookup interface { | |||||
| // HandleMQTTMessage processes an MQTT message: parses JSON array of RawReading or CSV. | // HandleMQTTMessage processes an MQTT message: parses JSON array of RawReading or CSV. | ||||
| // For JSON, converts each reading to BeaconAdvertisement and writes to the writer if MAC is in lookup. | // For JSON, converts each reading to BeaconAdvertisement and writes to the writer if MAC is in lookup. | ||||
| // Hostname is derived from topic (e.g. "publish_out/gateway1" -> "gateway1"). Safe if topic has no "/". | // Hostname is derived from topic (e.g. "publish_out/gateway1" -> "gateway1"). Safe if topic has no "/". | ||||
| func HandleMQTTMessage(topic string, payload []byte, lookup BeaconLookup, writer RawBeaconWriter) { | |||||
| func HandleMQTTMessage(topic string, payload []byte, lookup BeaconLookup, writer *kafka.Writer) { | |||||
| parts := strings.SplitN(topic, "/", 2) | parts := strings.SplitN(topic, "/", 2) | ||||
| hostname := "" | hostname := "" | ||||
| if len(parts) >= 2 { | if len(parts) >= 2 { | ||||
| @@ -58,7 +54,7 @@ func HandleMQTTMessage(topic string, payload []byte, lookup BeaconLookup, writer | |||||
| slog.Error("marshaling beacon advertisement", "err", err) | slog.Error("marshaling beacon advertisement", "err", err) | ||||
| break | break | ||||
| } | } | ||||
| if err := writer.WriteMessages(context.Background(), kafka.Message{Value: encoded}); err != nil { | |||||
| if err := kafkaclient.Write(context.Background(), writer, kafka.Message{Value: encoded}); err != nil { | |||||
| slog.Error("writing to Kafka", "err", err) | slog.Error("writing to Kafka", "err", err) | ||||
| time.Sleep(1 * time.Second) | time.Sleep(1 * time.Second) | ||||
| break | break | ||||
| @@ -7,6 +7,7 @@ import ( | |||||
| "net/http" | "net/http" | ||||
| "github.com/AFASystems/presence/internal/pkg/api/response" | "github.com/AFASystems/presence/internal/pkg/api/response" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| "gorm.io/gorm" | "gorm.io/gorm" | ||||
| @@ -45,8 +46,8 @@ func SettingsUpdateController(db *gorm.DB, writer *kafka.Writer, context context | |||||
| } | } | ||||
| kafkaMsg := kafka.Message{Value: eMsg} | kafkaMsg := kafka.Message{Value: eMsg} | ||||
| if err := writer.WriteMessages(context, kafkaMsg); err != nil { | |||||
| slog.Error("writing settings to Kafka", "err", err) | |||||
| if err := kafkaclient.Write(context, writer, kafkaMsg); err != nil { | |||||
| slog.Error("error writing settings to Kafka", "error", err) | |||||
| response.InternalError(w, "failed to publish settings update", err) | response.InternalError(w, "failed to publish settings update", err) | ||||
| return | return | ||||
| } | } | ||||
| @@ -3,11 +3,11 @@ package controller | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | |||||
| "log/slog" | "log/slog" | ||||
| "net/http" | "net/http" | ||||
| "github.com/AFASystems/presence/internal/pkg/api/response" | "github.com/AFASystems/presence/internal/pkg/api/response" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| @@ -17,17 +17,15 @@ import ( | |||||
| func SendKafkaMessage(writer *kafka.Writer, value *model.ApiUpdate, context context.Context) error { | func SendKafkaMessage(writer *kafka.Writer, value *model.ApiUpdate, context context.Context) error { | ||||
| valueStr, err := json.Marshal(&value) | valueStr, err := json.Marshal(&value) | ||||
| if err != nil { | if err != nil { | ||||
| msg := fmt.Sprintf("error in encoding: %v", err) | |||||
| slog.Error(msg) | |||||
| slog.Error("error encoding", "error", err) | |||||
| return err | return err | ||||
| } | } | ||||
| msg := kafka.Message{ | msg := kafka.Message{ | ||||
| Value: valueStr, | Value: valueStr, | ||||
| } | } | ||||
| if err := writer.WriteMessages(context, msg); err != nil { | |||||
| msg := fmt.Sprintf("Error in sending kafka message: %v", err) | |||||
| slog.Error(msg) | |||||
| if err := kafkaclient.Write(context, writer, msg); err != nil { | |||||
| slog.Error("error sending kafka message", "error", err) | |||||
| return err | return err | ||||
| } | } | ||||
| @@ -10,24 +10,20 @@ import ( | |||||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | "github.com/AFASystems/presence/internal/pkg/common/appcontext" | ||||
| "github.com/AFASystems/presence/internal/pkg/common/utils" | "github.com/AFASystems/presence/internal/pkg/common/utils" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| // AlertWriter writes decoded beacon events (e.g. to alertbeacons topic). | |||||
| type AlertWriter interface { | |||||
| WriteMessages(ctx context.Context, msgs ...kafka.Message) error | |||||
| } | |||||
| // ProcessIncoming decodes a beacon advertisement and writes the event to the writer if it changed. | // ProcessIncoming decodes a beacon advertisement and writes the event to the writer if it changed. | ||||
| func ProcessIncoming(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer AlertWriter, registry *model.ParserRegistry) { | |||||
| func ProcessIncoming(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer *kafka.Writer, registry *model.ParserRegistry) { | |||||
| if err := DecodeBeacon(adv, appState, writer, registry); err != nil { | if err := DecodeBeacon(adv, appState, writer, registry); err != nil { | ||||
| slog.Error("decoding beacon", "err", err, "id", adv.ID) | slog.Error("decoding beacon", "err", err, "id", adv.ID) | ||||
| } | } | ||||
| } | } | ||||
| // DecodeBeacon hex-decodes the payload, runs the parser registry, dedupes by event hash, and writes to writer. | // DecodeBeacon hex-decodes the payload, runs the parser registry, dedupes by event hash, and writes to writer. | ||||
| func DecodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer AlertWriter, registry *model.ParserRegistry) error { | |||||
| func DecodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer *kafka.Writer, registry *model.ParserRegistry) error { | |||||
| beacon := strings.TrimSpace(adv.Data) | beacon := strings.TrimSpace(adv.Data) | ||||
| id := adv.ID | id := adv.ID | ||||
| if beacon == "" { | if beacon == "" { | ||||
| @@ -63,7 +59,7 @@ func DecodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, | |||||
| return err | return err | ||||
| } | } | ||||
| if err := writer.WriteMessages(context.Background(), kafka.Message{Value: eMsg}); err != nil { | |||||
| if err := kafkaclient.Write(context.Background(), writer, kafka.Message{Value: eMsg}); err != nil { | |||||
| return fmt.Errorf("write alert: %w", err) | return fmt.Errorf("write alert: %w", err) | ||||
| } | } | ||||
| @@ -3,7 +3,6 @@ package kafkaclient | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | |||||
| "log/slog" | "log/slog" | ||||
| "sync" | "sync" | ||||
| @@ -21,15 +20,14 @@ func Consume[T any](r *kafka.Reader, ch chan<- T, ctx context.Context, wg *sync. | |||||
| default: | default: | ||||
| msg, err := r.ReadMessage(ctx) | msg, err := r.ReadMessage(ctx) | ||||
| if err != nil { | if err != nil { | ||||
| msg := fmt.Sprintf("error reading message: %v", err) | |||||
| slog.Error(msg) | |||||
| slog.Error("error reading message", "error", err) | |||||
| continue | continue | ||||
| } | } | ||||
| var data T | var data T | ||||
| if err := json.Unmarshal(msg.Value, &data); err != nil { | if err := json.Unmarshal(msg.Value, &data); err != nil { | ||||
| msg := fmt.Sprintf("error decoding: %v", err) | |||||
| slog.Error(msg) | |||||
| slog.Error("error decoding", "error", err) | |||||
| continue | continue | ||||
| } | } | ||||
| @@ -38,13 +38,17 @@ func InitKafkaManager() *KafkaManager { | |||||
| func (m *KafkaManager) AddKafkaWriter(kafkaUrl, topic string) { | func (m *KafkaManager) AddKafkaWriter(kafkaUrl, topic string) { | ||||
| kafkaWriter := &kafka.Writer{ | kafkaWriter := &kafka.Writer{ | ||||
| Addr: kafka.TCP(kafkaUrl), | |||||
| Topic: topic, | |||||
| Balancer: &kafka.LeastBytes{}, | |||||
| Async: false, | |||||
| RequiredAcks: kafka.RequireAll, | |||||
| BatchSize: 100, | |||||
| BatchTimeout: 10 * time.Millisecond, | |||||
| Addr: kafka.TCP(kafkaUrl), | |||||
| Topic: topic, | |||||
| Balancer: &kafka.LeastBytes{}, | |||||
| Async: false, | |||||
| RequiredAcks: kafka.RequireAll, | |||||
| BatchSize: 100, | |||||
| BatchTimeout: 10 * time.Millisecond, | |||||
| MaxAttempts: 5, | |||||
| WriteBackoffMin: 100 * time.Millisecond, | |||||
| WriteBackoffMax: 1 * time.Second, | |||||
| WriteTimeout: 5 * time.Second, | |||||
| } | } | ||||
| m.kafkaWritersMap.KafkaWritersLock.Lock() | m.kafkaWritersMap.KafkaWritersLock.Lock() | ||||
| @@ -0,0 +1,16 @@ | |||||
| package kafkaclient | |||||
| import ( | |||||
| "context" | |||||
| "time" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | |||||
| func Write(ctx context.Context, writer *kafka.Writer, message kafka.Message) error { | |||||
| ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |||||
| defer cancel() | |||||
| err := writer.WriteMessages(ctx, message) | |||||
| return err | |||||
| } | |||||
| @@ -7,26 +7,22 @@ import ( | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | "github.com/AFASystems/presence/internal/pkg/common/appcontext" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| // Score weights for location algorithm (configurable via constants). | // Score weights for location algorithm (configurable via constants). | ||||
| const ( | const ( | ||||
| SeenWeight = 1.5 | |||||
| RSSIWeight = 0.75 | |||||
| SeenWeight = 1.5 | |||||
| RSSIWeight = 0.75 | |||||
| DefaultDistance = 999 | DefaultDistance = 999 | ||||
| DefaultLastSeen = 999 | DefaultLastSeen = 999 | ||||
| ) | ) | ||||
| // LocationWriter writes location events (e.g. to Kafka). | |||||
| type LocationWriter interface { | |||||
| WriteMessages(ctx context.Context, msgs ...kafka.Message) error | |||||
| } | |||||
| // GetLikelyLocations runs the filter algorithm: scores beacons by RSSI and seen count, | // GetLikelyLocations runs the filter algorithm: scores beacons by RSSI and seen count, | ||||
| // updates app state with best location and confidence, and writes HTTPLocation to the writer. | // updates app state with best location and confidence, and writes HTTPLocation to the writer. | ||||
| func GetLikelyLocations(appState *appcontext.AppState, writer LocationWriter) { | |||||
| func GetLikelyLocations(appState *appcontext.AppState, writer *kafka.Writer) { | |||||
| ctx := context.Background() | ctx := context.Background() | ||||
| beacons := appState.GetAllBeacons() | beacons := appState.GetAllBeacons() | ||||
| settings := appState.GetSettingsValue() | settings := appState.GetSettingsValue() | ||||
| @@ -36,7 +32,7 @@ func GetLikelyLocations(appState *appcontext.AppState, writer LocationWriter) { | |||||
| Method: "Standard", | Method: "Standard", | ||||
| Distance: DefaultDistance, | Distance: DefaultDistance, | ||||
| ID: beacon.ID, | ID: beacon.ID, | ||||
| Location: "", | |||||
| Location: "", | |||||
| LastSeen: DefaultLastSeen, | LastSeen: DefaultLastSeen, | ||||
| } | } | ||||
| @@ -89,8 +85,8 @@ func GetLikelyLocations(appState *appcontext.AppState, writer LocationWriter) { | |||||
| continue | continue | ||||
| } | } | ||||
| if err := writer.WriteMessages(ctx, kafka.Message{Value: js}); err != nil { | |||||
| slog.Error("sending kafka location message", "err", err, "beacon_id", beacon.ID) | |||||
| if err := kafkaclient.Write(ctx, writer, kafka.Message{Value: js}); err != nil { | |||||
| slog.Error("error sending kafka location message", "error", err, "beacon_id", beacon.ID) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,6 @@ package location | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "crypto/tls" | "crypto/tls" | ||||
| "fmt" | |||||
| "net/http" | "net/http" | ||||
| "github.com/AFASystems/presence/internal/pkg/apiclient" | "github.com/AFASystems/presence/internal/pkg/apiclient" | ||||
| @@ -35,7 +34,6 @@ func NewDefaultInferencer(skipTLSVerify bool) *DefaultInferencer { | |||||
| // Infer gets a token and calls the inference API. | // Infer gets a token and calls the inference API. | ||||
| func (d *DefaultInferencer) Infer(ctx context.Context, cfg *config.Config) (model.PositionResponse, error) { | func (d *DefaultInferencer) Infer(ctx context.Context, cfg *config.Config) (model.PositionResponse, error) { | ||||
| if d.Token == "" { | if d.Token == "" { | ||||
| fmt.Printf("getting token\n") | |||||
| token, err := apiclient.GetToken(ctx, cfg, d.Client) | token, err := apiclient.GetToken(ctx, cfg, d.Client) | ||||
| if err != nil { | if err != nil { | ||||
| return model.PositionResponse{}, err | return model.PositionResponse{}, err | ||||
| @@ -43,5 +41,18 @@ func (d *DefaultInferencer) Infer(ctx context.Context, cfg *config.Config) (mode | |||||
| d.Token = token | d.Token = token | ||||
| } | } | ||||
| return apiclient.InferPosition(d.Token, d.Client, cfg) | |||||
| response, err := apiclient.InferPosition(d.Token, d.Client, cfg) | |||||
| if err != nil { | |||||
| token, err := apiclient.GetToken(ctx, cfg, d.Client) | |||||
| if err != nil { | |||||
| return model.PositionResponse{}, err | |||||
| } | |||||
| d.Token = token | |||||
| response, err = apiclient.InferPosition(d.Token, d.Client, cfg) | |||||
| if err != nil { | |||||
| return model.PositionResponse{}, err | |||||
| } | |||||
| } | |||||
| return response, nil | |||||
| } | } | ||||
| @@ -10,21 +10,18 @@ import ( | |||||
| "strings" | "strings" | ||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/google/uuid" | "github.com/google/uuid" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| "gorm.io/gorm" | "gorm.io/gorm" | ||||
| ) | ) | ||||
| // KafkaWriter defines the interface for writing Kafka messages (allows mocking in tests) | |||||
| type KafkaWriter interface { | |||||
| WriteMessages(ctx context.Context, msgs ...kafka.Message) error | |||||
| } | |||||
| func findTracker(msg model.HTTPLocation, db *gorm.DB) (model.Tracker, error) { | func findTracker(msg model.HTTPLocation, db *gorm.DB) (model.Tracker, error) { | ||||
| fmt.Printf("Finding tracker for MAC: %s, ID: %s\n", msg.MAC, msg.ID) | |||||
| var tracker model.Tracker | var tracker model.Tracker | ||||
| if msg.MAC != "" { | if msg.MAC != "" { | ||||
| if err := db.Where("mac = ?", msg.MAC).Find(&tracker).Error; err != nil { | |||||
| if err := db.Where("mac = ?", strings.ToUpper(strings.ReplaceAll(msg.MAC, ":", ""))).Find(&tracker).Error; err != nil { | |||||
| return model.Tracker{}, err | return model.Tracker{}, err | ||||
| } | } | ||||
| @@ -56,7 +53,7 @@ func findZones(trackerID string, db *gorm.DB) ([]string, error) { | |||||
| return allowedZones, nil | return allowedZones, nil | ||||
| } | } | ||||
| func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer KafkaWriter, ctx context.Context) { | |||||
| func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { | |||||
| tracker, err := findTracker(msg, db) | tracker, err := findTracker(msg, db) | ||||
| if err != nil { | if err != nil { | ||||
| msg := fmt.Sprintf("Error in finding tracker: %v", err) | msg := fmt.Sprintf("Error in finding tracker: %v", err) | ||||
| @@ -95,7 +92,7 @@ func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer KafkaWr | |||||
| sendAlert(gw.ID, msg.ID, writer, ctx, allowedZones, db) | sendAlert(gw.ID, msg.ID, writer, ctx, allowedZones, db) | ||||
| } | } | ||||
| func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer KafkaWriter, ctx context.Context) { | |||||
| func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { | |||||
| tracker, err := findTracker(msg, db) | tracker, err := findTracker(msg, db) | ||||
| if err != nil { | if err != nil { | ||||
| msg := fmt.Sprintf("Error in finding tracker: %v", err) | msg := fmt.Sprintf("Error in finding tracker: %v", err) | ||||
| @@ -133,7 +130,7 @@ func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer Kafka | |||||
| sendAlert(gw.ID, tracker.ID, writer, ctx, allowedZones, db) | sendAlert(gw.ID, tracker.ID, writer, ctx, allowedZones, db) | ||||
| } | } | ||||
| func sendAlert(gwId, trackerId string, writer KafkaWriter, ctx context.Context, allowedZones []string, db *gorm.DB) { | |||||
| func sendAlert(gwId, trackerId string, writer *kafka.Writer, ctx context.Context, allowedZones []string, db *gorm.DB) { | |||||
| if len(allowedZones) != 0 && !slices.Contains(allowedZones, gwId) { | if len(allowedZones) != 0 && !slices.Contains(allowedZones, gwId) { | ||||
| alert := model.Alert{ | alert := model.Alert{ | ||||
| ID: uuid.New().String(), | ID: uuid.New().String(), | ||||
| @@ -157,7 +154,11 @@ func sendAlert(gwId, trackerId string, writer KafkaWriter, ctx context.Context, | |||||
| msg := kafka.Message{ | msg := kafka.Message{ | ||||
| Value: eMsg, | Value: eMsg, | ||||
| } | } | ||||
| writer.WriteMessages(ctx, msg) | |||||
| if err := kafkaclient.Write(ctx, writer, msg); err != nil { | |||||
| msg := fmt.Sprintf("Error in writing message: %v", err) | |||||
| slog.Error(msg) | |||||
| return | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -4,11 +4,12 @@ import ( | |||||
| "context" | "context" | ||||
| "encoding/json" | "encoding/json" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| func SendParserConfig(kp model.KafkaParser, writer KafkaWriter, ctx context.Context) error { | |||||
| func SendParserConfig(kp model.KafkaParser, writer *kafka.Writer, ctx context.Context) error { | |||||
| eMsg, err := json.Marshal(kp) | eMsg, err := json.Marshal(kp) | ||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| @@ -17,7 +18,7 @@ func SendParserConfig(kp model.KafkaParser, writer KafkaWriter, ctx context.Cont | |||||
| Value: eMsg, | Value: eMsg, | ||||
| } | } | ||||
| if err := writer.WriteMessages(ctx, msg); err != nil { | |||||
| if err := kafkaclient.Write(ctx, writer, msg); err != nil { | |||||
| return err | return err | ||||
| } | } | ||||
| return nil | return nil | ||||
| @@ -1,20 +1,20 @@ | |||||
| #!/bin/bash | #!/bin/bash | ||||
| # Build the server | # Build the server | ||||
| docker build -t afasystemadmin/ble-ai-localizer:server_v1 -f ../../build/package/Dockerfile.server ../../ | |||||
| docker build -t afasystemadmin/ble-ai-localizer:server_v1.1 -f ../../build/package/Dockerfile.server ../../ | |||||
| ¸ | |||||
| # Build the location | # Build the location | ||||
| docker build -t afasystemadmin/ble-ai-localizer:location_v1 -f ../../build/package/Dockerfile.location ../../ | |||||
| docker build -t afasystemadmin/ble-ai-localizer:location_v1.1 -f ../../build/package/Dockerfile.location ../../ | |||||
| # Build the decoder | # Build the decoder | ||||
| docker build -t afasystemadmin/ble-ai-localizer:decoder_v1 -f ../../build/package/Dockerfile.decoder ../../ | |||||
| docker build -t afasystemadmin/ble-ai-localizer:decoder_v1.1 -f ../../build/package/Dockerfile.decoder ../../ | |||||
| # Build the bridge | # Build the bridge | ||||
| docker build -t afasystemadmin/ble-ai-localizer:bridge_v1 -f ../../build/package/Dockerfile.bridge ../../ | |||||
| docker build -t afasystemadmin/ble-ai-localizer:bridge_v1.1 -f ../../build/package/Dockerfile.bridge ../../ | |||||
| docker image ls | docker image ls | ||||
| docker push afasystemadmin/ble-ai-localizer:server_v1 | |||||
| docker push afasystemadmin/ble-ai-localizer:location_v1 | |||||
| docker push afasystemadmin/ble-ai-localizer:decoder_v1 | |||||
| docker push afasystemadmin/ble-ai-localizer:bridge_v1 | |||||
| docker push afasystemadmin/ble-ai-localizer:server_v1.1 | |||||
| docker push afasystemadmin/ble-ai-localizer:location_v1.1 | |||||
| docker push afasystemadmin/ble-ai-localizer:decoder_v1.1 | |||||
| docker push afasystemadmin/ble-ai-localizer:bridge_v1.1 | |||||
| @@ -0,0 +1 @@ | |||||
| docker exec db pg_dump -U postgres -F c postgres > dump.sql | |||||
| @@ -0,0 +1,3 @@ | |||||
| docker cp dump.sql postgres:/tmp/dump.sql | |||||
| docker exec -t postgres pg_restore -U postgres -d postgres /tmp/dump.sql | |||||