---
module: agent.log-collect
title: Log Collect Path — on-demand file/log pull from an arbitrary ATM path
tab: Architect
order: 30
audience: dev
---

> **Internal · how it's built.** Confirmed against `hiveops-module-log-collect` source (`com.hiveops.logcollect`) 2026-07-01. See [technical.agent] for agent-wide context (module system, HTTP layer, platform abstraction).

## Maven module

`hiveops-module-log-collect` (artifact `hiveops-module-log-collect`, own version `1.0.0`, parented by `hiveops-agent-parent` — currently `4.4.3`). Packaged as a standalone jar (`<finalName>${project.artifactId}</finalName>` → `hiveops-module-log-collect.jar`).

Dependencies (all `provided` scope — supplied by the agent runtime, not shaded in):

- `hiveops-core` — module SPI + HTTP layer (`FileUploadClient`, `HttpClientHelper`, `HttpClientSettings`, `CurrentAtmPendingTask`).
- `hiveops-agent-journal` (`1.0.0`) — for `com.hiveops.logs.AgentLogUploader`, which does the actual ZIP-and-upload.
- `log4j-api`, `commons-lang3`.

Registered via ServiceLoader SPI: `src/main/resources/META-INF/services/com.hiveops.core.module.AgentModule` → `com.hiveops.logcollect.LogCollectPathModule`.

## The one class

There is a single class, `LogCollectPathModule`, implementing `AgentModule`. No sub-packages, no monitors, no emitters — the heavy lifting lives in `AgentLogUploader` (from `hiveops-agent-journal`).

### Lifecycle (`AgentModule` contract)

- `getName()` → **`log-collect-path`**
- `getVersion()` → `1.0.0`
- `getLogPackage()` → `com.hiveops.logcollect`
- `getDependencies()` → empty (no ordering constraints)
- `isEnabled(ctx)` → true when `agent.endpoint` **or** `incident.endpoint` is non-blank in the main properties.
- `initialize(ctx)` → picks the settings prefix (`agent` if `agent.endpoint` is set, else `incident`), builds an `HttpClientSettings` via `HttpClientHelper.loadSettingsFromProperties(props, prefix)`, and caches `country` + `atmName` from the `ModuleContext`.
- `start()` → creates a single-thread daemon `ScheduledExecutorService` (thread name `log-collect-path`) and schedules `poll()` at a fixed `POLL_INTERVAL_SECONDS = 30` rate.
- `stop()` → `scheduler.shutdownNow()`.

## Poll → detect → collect → emit flow

```
every 30s  poll()
  └─ FileUploadClient.queryCurrentAtmPendingTask(country, atmName, "LOG_COLLECT_PATH")
       GET {endpoint}/api/agent/{country}/{atm}/task?kind=LOG_COLLECT_PATH
  └─ null / blank taskKind  → return (nothing pending; HTTP 204 → null)
  └─ task present           → handle(client, task)
        ├─ targetPath = task.configPatch.get("path")
        │     └─ blank → statusReport(FAILED, "No path specified…")
        ├─ statusReport(tid, aid, "RUNNING", "Collecting logs from: <path>")
        ├─ AgentLogUploader.uploadPath(targetPath, settings, country, atmName, tid)
        │     POST {endpoint}/api/agent/{country}/{atm}/log/upload?taskId={tid}
        └─ statusReport(tid, aid, "COMPLETED", "…uploaded successfully")
              └─ on exception → statusReport(FAILED, e.getMessage())
```

### `AgentLogUploader.uploadPath(...)` (in `hiveops-agent-journal`)

1. Normalize `\`→`/` and resolve the path. Missing path → `IOException("Path does not exist…")`.
2. If it's a regular file, collect just that file; otherwise `Files.walkFileTree` collecting **every** file. `visitFileFailed` logs and skips unreadable files (`CONTINUE`).
3. Empty result → `IOException("No files found at path…")`.
4. Build a temp ZIP (`hiveops-path-logs-*.zip`) with entries relative to the target root (single-file case roots at the parent).
5. Multipart-POST the ZIP to `…/log/upload?taskId={tid}` — boundary `----HiveOpsLogUploadBoundary`, form part name `file`, filename `hiveops-logs.zip`, `Content-Type: application/zip`. Auth headers/host applied via `HttpClientHelper.applyParameters` / `applyParameterHost`.
6. Non-2xx logs the error body; the temp ZIP is always deleted in `finally`.

The `taskId` query param is what lets the backend attach the upload to a trackable `AtmLogCollect` record for that task.

## How events reach the platform

This module reaches the platform over the **agent HTTP task API**, not the journal-event pipeline. Three calls, all against `settings.getEndpoint()` (the `agent.endpoint`/`incident.endpoint` base — served by **agent-proxy** in production, which proxies to incident):

| Purpose | Method + path |
|---------|---------------|
| Poll for work | `GET /api/agent/{country}/{atm}/task?kind=LOG_COLLECT_PATH` |
| Status updates | `POST /api/agent/tasks/{tid}/status`  body `{ status, message }` |
| Deliver the bundle | `POST /api/agent/{country}/{atm}/log/upload?taskId={tid}` (multipart ZIP) |

Errors in `poll()` are classified: `IOException`/`SocketException` → warn ("transient network error"); `com.hiveops.http.Utils.MyException` (backend error) → warn ("server error"); anything else → error with stack trace. The poll loop survives all of them and retries on the next tick.

## Platform abstraction

The module itself uses **no** `PlatformService` — it operates on whatever path string the task supplies and relies on `AgentLogUploader`'s `\`→`/` normalization to accept Windows paths. (By contrast the `INSTALL_MODULE` handler that *delivers* this jar does use `PlatformService` for `restartService` / scripts path.)

## `CurrentAtmPendingTask` fields used

`tid`, `aid`, `taskKind`, and `configPatch` (a `Map<String,String>`; only key `path` is read). Other fields on the DTO (`kb`, `paths`, `artifactCdnUrl`, `cdnToken`) are ignored by this module.
