Skip to content

Log Shipping

Log Shipping collects diagnostic logs from the Breeze agent and stores them centrally for querying, correlation, and troubleshooting. Every agent component — heartbeat, commands, patching, enrollment, discovery, terminal, and more — writes structured log entries through Go’s slog package. The logging subsystem intercepts these entries, buffers them in memory, compresses them with gzip, and ships them to the API in batches every 60 seconds. On the server side, logs are stored in PostgreSQL with indexes on device, timestamp, level, and component, making it fast to search across thousands of devices.

This is distinct from the fleet event log system (which collects Windows Event Log, syslog, and application logs from managed devices). Log Shipping specifically handles the Breeze agent’s own diagnostic output — the internal telemetry that helps you understand what the agent is doing, debug enrollment failures, diagnose command execution issues, and monitor agent health across your fleet.


| Level | Description | Numeric Priority | |---|---|---| | debug | Verbose diagnostic output for development and deep troubleshooting | Lowest | | info | Normal operational messages (startup, enrollment, heartbeat success) | Default | | warn | Non-fatal issues that may indicate a problem (retry, degraded state) | Medium | | error | Failures that require attention (command execution failed, connection lost) | Highest |

| Field | Type | Max Length | Description | |---|---|---|---| | timestamp | ISO 8601 datetime | — | When the log entry was created on the agent | | level | enum | — | One of: debug, info, warn, error | | component | string | 100 chars | Which agent subsystem produced the entry (e.g., heartbeat, commands, patching) | | message | string | 10,000 chars | Human-readable log message | | fields | JSON | 32 KB | Structured key-value data (command IDs, durations, error details, etc.) | | agentVersion | string | 50 chars | Version of the agent that produced the entry |

| Component | What It Logs | |---|---| | heartbeat | Heartbeat send/receive, metric collection, cert renewal | | commands | Command dispatch, execution, result reporting | | patching | Patch scan, download, install, reboot handling | | enrollment | Enrollment attempts, token exchange, config persistence | | discovery | Network scan execution, host detection, result shipping | | terminal | PTY allocation, resize, data relay | | desktop | Remote desktop session lifecycle, WebRTC signaling | | updater | Self-update checks, downloads, binary replacement | | mtls | Certificate loading, renewal, TLS configuration | | logging | Log shipper lifecycle, buffer management |


The agent’s logging subsystem has three layers:

The agent writes logs to stdout (or a file via RotatingWriter) using Go’s slog package. The format is configurable as text (default) or json.

// Create a component logger
logger := logging.L("heartbeat")
logger.Info("heartbeat sent", "statusCode", 200, "durationMs", 45)

When file logging is enabled, the RotatingWriter handles size-based rotation:

| Setting | Default | Description | |---|---|---| | Max file size | 50 MB | Rotates when the log file exceeds this size | | Max backups | 3 | Keeps up to 3 rotated files (.1, .2, .3) | | File permissions | 0600 | Owner read/write only | | Directory permissions | 0700 | Owner read/write/execute only |

Rotation works by shifting existing backups (.3 is deleted, .2 becomes .3, etc.) and renaming the current file to .1.

The Shipper intercepts all log entries via a custom slog.Handler wrapper (shippingHandler) and forwards them to the API:

  1. Every slog call passes through the shippingHandler, which writes to the local handler (stdout/file) and also enqueues the entry for remote shipping if it meets the minimum level threshold.

  2. Entries are buffered in a channel with capacity for 500 entries. If the buffer is full, entries are dropped and a counter is incremented. The dropped count is reported in the next heartbeat.

  3. Every 60 seconds (or when 500 entries accumulate), the shipper flushes the buffer. Entries are serialized as JSON and gzip-compressed.

  4. The compressed batch is POSTed to POST /api/v1/agents/:agentId/logs with Content-Encoding: gzip and Authorization: Bearer headers.

  5. On failure, the shipper retries up to 2 additional times with 1-second backoff plus random jitter. Server errors (5xx) and network errors trigger retries. Client errors (4xx) do not.

  6. On graceful shutdown, the shipper drains remaining buffered entries and ships them before exiting.


Agents ship logs to:

POST /api/v1/agents/:agentId/logs
Content-Type: application/json
Content-Encoding: gzip
Authorization: Bearer AGENT_TOKEN
{
"logs": [
{
"timestamp": "2026-02-15T14:30:00.000Z",
"level": "info",
"component": "heartbeat",
"message": "heartbeat sent successfully",
"fields": {
"statusCode": 200,
"durationMs": 45
},
"agentVersion": "1.2.0"
}
]
}

| Limit | Value | |---|---| | Max entries per request | 200 | | Max request body size | 256 KB | | Max decompressed payload | 10 MB | | Max message length | 10,000 characters | | Max component length | 100 characters | | Max fields JSON size | 32 KB | | Batch insert size | 100 rows per database insert |

If the API returns 429 (per-agent or per-org rate limit) or 503, the agent honors the server’s Retry-After header and waits the requested duration before retrying — capped at 300 seconds so a misconfigured server can’t park agents indefinitely. When the header is absent, the agent falls back to its normal exponential backoff schedule.

| Status | Meaning | |---|---| | 201 | All logs accepted and stored | | 200 | Empty batch (0 logs) — no-op | | 207 | Partial success — some logs stored, some failed | | 400 | Invalid request body, decompression failure, or validation error | | 404 | Agent/device not found | | 500 | All logs failed to insert |


Diagnostic logs are queried per-device through the devices API:

Terminal window
curl "/api/v1/devices/:deviceId/diagnostic-logs?level=warn,error&component=patching&since=2026-02-01T00:00:00Z&search=failed" \
-H "Authorization: Bearer $TOKEN"

| Parameter | Type | Description | |---|---|---| | level | string | Comma-separated levels to include (e.g., warn,error) | | component | string | Exact component name to filter by | | since | ISO 8601 datetime | Only logs at or after this time | | until | ISO 8601 datetime | Only logs at or before this time | | search | string | Case-insensitive text search across message and fields | | page | integer | Page number (default 1) | | limit | integer | Results per page (default 1000) |

{
"logs": [
{
"id": "uuid",
"deviceId": "uuid",
"orgId": "uuid",
"timestamp": "2026-02-15T14:30:00.000Z",
"level": "error",
"component": "patching",
"message": "patch installation failed",
"fields": {
"patchId": "KB5034441",
"exitCode": 1603,
"error": "HRESULT 0x80070005: Access denied"
},
"agentVersion": "1.2.0",
"createdAt": "2026-02-15T14:30:05.000Z"
}
],
"total": 47,
"limit": 1000,
"offset": 0
}

For searching logs across multiple devices and organizations, use the fleet log search endpoint:

Terminal window
curl -X POST /api/v1/logs/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "patch installation failed",
"timeRange": {
"start": "2026-02-01T00:00:00Z",
"end": "2026-02-28T23:59:59Z"
},
"level": ["error", "critical"],
"category": ["system"],
"deviceIds": ["UUID1", "UUID2"],
"limit": 100,
"sortBy": "timestamp",
"sortOrder": "desc"
}'

| Feature | Endpoint | Description | |---|---|---| | Aggregation | GET /api/v1/logs/aggregation | Time-bucketed counts grouped by level, category, source, or device | | Trends | GET /api/v1/logs/trends | Top error patterns and trending log sources | | Correlation detection | POST /api/v1/logs/correlation/detect | Find patterns appearing across multiple devices | | Correlation list | GET /api/v1/logs/correlation | List detected correlations with status | | Saved queries | GET/POST/DELETE /api/v1/logs/queries | Save and reuse common search filters |


When the agent’s buffer is full (500 entries), new log entries are dropped. The agent tracks dropped entries and reports the count in each heartbeat. After a successful heartbeat, the counter resets to zero.

You can monitor this by checking the heartbeat data for a device. A non-zero dropped log count indicates the agent is producing logs faster than it can ship them. Common causes:

  • Network latency to the API server is high
  • Debug-level logging is enabled, producing high volume
  • Agent is under heavy load (e.g., running many concurrent commands)

To mitigate, increase the shipping level to warn or error, or investigate the root cause of the high log volume.


| Method | Path | Description | |---|---|---| | POST | /api/v1/agents/:id/logs | Ship a batch of diagnostic log entries |

| Method | Path | Description | |---|---|---| | GET | /api/v1/devices/:id/diagnostic-logs | Query diagnostic logs for a specific device |

| Method | Path | Description | |---|---|---| | POST | /api/v1/logs/search | Search logs across the fleet with filters | | GET | /api/v1/logs/aggregation | Time-bucketed log aggregation | | GET | /api/v1/logs/trends | Trending log patterns | | POST | /api/v1/logs/correlation/detect | Detect cross-device log correlations | | GET | /api/v1/logs/correlation/detect/:jobId | Check status of an async correlation detection job | | GET | /api/v1/logs/correlation | List detected correlations | | GET | /api/v1/logs/queries | List saved log search queries | | POST | /api/v1/logs/queries | Create a saved log search query | | GET | /api/v1/logs/queries/:id | Get a saved query by ID | | DELETE | /api/v1/logs/queries/:id | Delete a saved query |


Logs not appearing for a device Verify the agent is enrolled and the log shipper is initialized. The shipper starts after enrollment when the agent has a valid server URL, agent ID, and auth token. Check the agent’s local stdout for [log-shipper] messages indicating shipping errors.

Only seeing info-level and above The default minimum shipping level is info. To capture debug entries, dynamically adjust the shipping level via the agent’s configuration or use SetShipperLevel("debug"). Note that debug-level logging significantly increases volume and may cause buffer drops.

Logs delayed by up to 60 seconds This is expected. The shipper flushes every 60 seconds or when the buffer reaches 500 entries, whichever comes first. For time-critical debugging, lower the minimum level to produce more entries and trigger faster flushes.

“Device not found” when agent ships logs The ingest endpoint looks up the device by agentId (the :id path parameter). If the agent was recently re-enrolled with a new agent ID, the old ID will return 404. Verify the agent’s configuration file has the correct agent ID.

Search returns no results despite logs existing The search parameter on the diagnostic logs endpoint uses PostgreSQL ILIKE against the message column and the fields JSON cast to text. Ensure your search term does not contain special SQL characters. Also verify the since/until time range includes the logs you are looking for.

High dropped log count in heartbeat The agent’s buffer holds 500 entries. If the shipper cannot deliver batches fast enough (network issues, API downtime), entries are dropped. The dropped count resets after each successful heartbeat. Investigate network connectivity between the agent and API, or raise the minimum shipping level to reduce volume.