---
module: agent.log
title: Agent Log — self-log upload & error-to-incident monitoring
tab: Architect
order: 30
audience: dev
---

> **Internal · how it's built.** Confirmed against `hiveops-module-log` source (`com.hiveops.logs`) 2026-07-01. See [technical.agent] for agent-wide module/lifecycle context.

## Maven module & packaging

`hiveops-module-log` (artifact `hiveops-module-log`, own version `1.0.0`, parented by `hiveops-agent-parent` `4.4.3`). Built as a standalone jar with `<finalName>${project.artifactId}</finalName>` → `hiveops-module-log.jar`, a drop-in for the agent's `modules/` directory (not merged into the fat JAR).

Dependencies are all **`provided`** scope — supplied by the running fat JAR, not bundled:
- `hiveops-core` — the `AgentModule` SPI + `HttpClientHelper`/`HttpClientSettings`.
- `hiveops-agent-journal` — provides **`AgentLogUploader`** (the multipart/zip upload helper `LogUploadModule` delegates to). *Note: `hiveops-agent-journal` is in the `atm` build set but not the `srv` set, while `hiveops-module-log` is in both; whether the srv fat JAR still carries `AgentLogUploader` on the classpath is **unverified** — flag if a srv device throws `NoClassDefFoundError` on log upload.*
- `log4j-api`, `commons-lang3`, `commons-io`.

Two implementations are registered via the ServiceLoader SPI file `META-INF/services/com.hiveops.core.module.AgentModule`:
```
com.hiveops.logs.LogUploadModule
com.hiveops.logs.LogMonitorModule
```

## Classes and flow

| Class | Role |
|---|---|
| `LogUploadModule` | `AgentModule` `log-upload` — scheduler that ships the whole log file on an interval. |
| `LogMonitorModule` | `AgentModule` `log-monitor` (depends on `log-upload`) — scheduler that tails the log for errors and reports summaries. |
| `AgentLogUploader` | *(in `hiveops-agent-journal`)* static helper doing the actual multipart POST / ZIP bundling. Shared with the fleet log-collect commands. |

Neither module overrides `getLogPackage()`, so their own log output goes to the **root** logger (`hiveops-agent.log`) rather than a dedicated `logs/modules/*.log` — which is deliberate here, since that root file is exactly what they monitor and upload. Neither overrides `isHealthy()` either, so the `ModuleWatchdog` treats them as healthy by default even if their scheduler thread dies *(potential blind spot — unverified as an operational issue)*.

### log-upload cycle (poll/push, interval-driven)

1. `initialize()`: resolve endpoint (`agent.endpoint` else `incident.endpoint`; disable if neither), build `HttpClientSettings` via `HttpClientHelper.loadSettingsFromProperties(props, prefix)` where `prefix` is `"agent"` or `"incident"`, read `country`/`atmName` from `ModuleContext`, read `log.upload.interval.minutes` (default 15), resolve `logFilePath`.
2. `start()`: single daemon thread `log-upload`, `scheduleAtFixedRate(doUpload, 0, interval, MINUTES)` — **fires immediately** then every interval.
3. `doUpload()` → `AgentLogUploader.upload(logFilePath, settings, country, atmName)`:
   - Skips silently if the file is absent.
   - Multipart POST to `{endpoint}/api/agent/{country}/{atm}/log/upload` (boundary `----HiveOpsLogUploadBoundary`, part `file`, filename `hiveops-agent.log`, `Content-Type: text/plain`); logs HTTP status/elapsed/bytes.
4. `stop()`: `scheduler.shutdownNow()`.

### log-monitor cycle (tail → detect → report)

1. `initialize()`: same endpoint/settings resolution; read the four `log.monitor.*` tunables; resolve `logFilePath`.
2. `start()`: daemon thread `log-monitor`, `scheduleAtFixedRate(doMonitor, interval, interval, MINUTES)` — **first run delayed one interval** so the log settles.
3. `doMonitor()`:
   - Track `lastCheckedPosition` (byte offset). If file shrank (`position > size`) → treat as rotation, reset to 0. If `position == size` → nothing new, return.
   - `scanNewLines(fileSize)`: `RandomAccessFile.seek(lastCheckedPosition)`, read line-by-line, re-decode each line from ISO-8859-1 bytes → UTF-8 (works around `RandomAccessFile.readLine()`'s charset), match against `LOG_LEVEL_PATTERN` = `^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[.,]\d+\s+(ERROR|FATAL|WARN)\s+(.+)$`. `WARN` → `warnCount`; `ERROR`/`FATAL` → `errorCount` (+ up to `maxSamples` sample lines, each truncated to 200 chars).
   - Advance `lastCheckedPosition = fileSize`.
   - If `errorCount < errorThreshold` → return. If `lastReportedAt > 0` and within `cooldownMinutes` → return.
   - Else `reportToBackend()` and set `lastReportedAt = now`.
4. `reportToBackend()`: hand-builds JSON `{"errorCount":N,"warnCount":N,"sampleErrors":[...]}` (no Jackson — its own `jsonEscape`), `POST` with `Content-Type: application/json` to `String.format("%s/atm/log-error?country=%s&name=%s", endpoint, country, name)`. Accepts HTTP 200/201; logs (truncated) error body otherwise. Never throws out of the scheduler.

All state (`lastCheckedPosition`, `lastReportedAt`) is **in-memory only** — an agent restart resets the tail offset to 0, so the first post-restart scan re-reads the whole current file.

## How events/results reach the platform

This module has **no** journal/incident `EventType` (`IAT_*`, `CARD_READER_*`, …) integration and does **not** use `IncidentEventClient` — see [technical.agent] "API surface" for that separate path. It also does not report fleet task status. It has exactly two HTTP touchpoints, both terminating at **agent-proxy** (which owns agent log endpoints natively and forwards to incident's internal API):

```
LogUploadModule ──► AgentLogUploader.upload()
   POST {endpoint}/api/agent/{country}/{atm}/log/upload   (multipart)
       │
       ▼  agent-proxy AgentLogController.uploadLog()  (50 MB cap → 413)
          → IncidentInternalService.uploadLog() → incident internal store

LogMonitorModule ──► reportToBackend()
   POST {endpoint}/atm/log-error?country=&name=           (JSON summary)
       │  ⚠ mismatch: agent-proxy's error endpoint is
       │     POST /api/agent/{country}/{atm}/log/errors
       ▼  agent-proxy AgentLogController.logErrors()
          → IncidentInternalService.logErrors()
          → incident InternalAgentController POST /api/internal/agent/{country}/{atm}/log/errors
          → LogErrorReportRequest → IncidentAutoCreationService → SOFTWARE_ERROR incident
```

**Path discrepancy to be aware of:** the *upload* URL matches agent-proxy's route exactly, but the *monitor's* report URL (`/atm/log-error?country=&name=`) is a legacy shape that does **not** match agent-proxy's `/api/agent/{country}/{atm}/log/errors`. Whether an nginx/proxy rewrite bridges the two, or whether error reporting silently 404s on the current fleet, is **unverified** — verify against the live agent-proxy/nginx config before relying on error-to-incident from this module.

## Related helper capabilities (not driven by this module)

`AgentLogUploader` also exposes `uploadDirectory()` (bundles every `*.log`/`*.log.gz` under `logs/` incl. `logs/modules/` into `hiveops-logs.zip`) and `uploadPath(..., taskId)` (zips an arbitrary path, appends `?taskId=`). These back the **`LOG_COLLECT` / `LOG_COLLECT_PATH` fleet commands** and are invoked from the fleet command handlers, **not** from `log-upload`/`log-monitor`. The scheduled upload only ever ships the single current `hiveops-agent.log`.

## Platform abstraction

The module does **not** use `PlatformServiceFactory` / `PlatformService` at all — it needs no registry reads, path resolution, or shell-outs. Log path resolution is a plain JVM-system-property (`hiveops-agent.log`) lookup with a `ModuleContext.getStartedFromDir()` fallback, and all I/O is JDK (`RandomAccessFile`, `HttpURLConnection`, `Files`).
