| Status | Active |
|---|---|
| Owner | HyperFleet Platform Team |
| Last Updated | 2026-01-28 |
This document defines the standard logging approach for all HyperFleet components (API, Sentinel, Adapters).
- Consistency: All components configure logging the same way
- Traceability: Distributed tracing via
trace_idand correlation fields - Observability: Structured logs that integrate with log aggregation systems
- Creating a shared logging library
- Mandating a specific logging framework
Shared libraries (e.g., broker client) MUST inherit the logging context from the calling component:
- When Sentinel publishes to the broker → logs include
component=sentinel - When an Adapter subscribes from the broker → logs include
component=adapter-validation
The shared library should not set its own component value - it uses the context provided by the caller.
Example:
// ✅ DO: Caller creates logger with context and passes it to the library
logger := slog.With("component", "sentinel", "subset", "clusters")
broker.Publish(ctx, event, broker.WithLogger(logger))
// ✅ DO: Shared library uses the passed logger (preserves caller context)
func (b *Broker) Publish(ctx context.Context, event Event, opts ...Option) {
cfg := applyOptions(opts)
cfg.Logger.Info("publishing event", "topic", b.topic)
}
// ❌ DON'T: Shared library creating its own logger loses caller context
func (b *Broker) Publish(ctx context.Context, event Event) {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("publishing event") // Missing component, subset, trace_id, etc.
}All components MUST support configuration via command-line flags and environment variables. Configuration files are optional.
| Option | Flag | Environment Variable | Default | Description |
|---|---|---|---|---|
| Log Level | --log-level |
HYPERFLEET_LOG_LEVEL |
info |
Minimum level: debug, info, warn, error |
| Log Format | --log-format |
HYPERFLEET_LOG_FORMAT |
json |
Output format: text or json |
| Log Output | --log-output |
HYPERFLEET_LOG_OUTPUT |
stdout |
Destination: stdout or stderr |
Precedence (highest to lowest): flags → environment variables → config file → defaults
For production, use LOG_FORMAT=json for better log aggregation.
Ordered by severity (lowest to highest):
| Level | Description | Examples |
|---|---|---|
debug |
Detailed debugging | Variable values, event payloads |
info |
Operational information | Startup, successful operations |
warn |
Warning conditions | Retry attempts, slow operations |
error |
Error conditions | Failures, invalid configuration |
When LOG_LEVEL is set, only messages at that level or higher are output.
All log entries MUST include:
| Field | Type | Description |
|---|---|---|
timestamp |
RFC3339 | When created (UTC) |
level |
string | Log level |
message |
string | Human-readable message |
component |
string | Component name (api, sentinel, adapter-validation) |
version |
string | Component version |
hostname |
string | Pod name or hostname |
Include when available for distributed tracing:
| Field | Scope | Description |
|---|---|---|
trace_id |
Distributed | OpenTelemetry trace ID (propagated across services) |
span_id |
Distributed | Current span identifier |
request_id |
Single service | HTTP request identifier (API only) |
event_id |
Adapters | CloudEvents ID (from received event) |
Include when the log entry relates to a HyperFleet resource:
| Field | Description |
|---|---|
cluster_id |
Cluster identifier |
resource_type |
Resource type (clusters, nodepools) |
resource_id |
Resource identifier |
Note: For Cluster resources,
cluster_idis sufficient. For child resources (e.g., NodePools), includeresource_typeandresource_idto identify the specific resource.
Include when logging errors:
| Field | Type | Description |
|---|---|---|
error |
string | Error message |
stack_trace |
array | Stack trace (only for unexpected errors or debug level) |
request_context |
object | Relevant request/payload data for debugging (sensitive data MUST be masked) |
Note: When logging errors, include enough context to investigate incidents without needing to reproduce the issue. Always mask sensitive data per the Sensitive Data section.
For local development:
{timestamp} {LEVEL} [{component}] [{version}] [{hostname}] {message} {key=value}...
2025-01-15T10:30:00.123Z INFO [sentinel] [v1.2.3] [sentinel-7d4b8c6f5] Publishing event subset=clusters cluster_id=cls-123
2025-01-15T10:30:05.456Z ERROR [sentinel] [v1.2.3] [sentinel-7d4b8c6f5] Failed to publish subset=clusters error="connection refused"
2025-01-15T10:30:05.456Z ERROR [sentinel] [v1.2.3] [sentinel-7d4b8c6f5] Unexpected error subset=clusters error="nil pointer"
main.processCluster() processor.go:89
main.reconcileLoop() loop.go:45
For log aggregation:
{
"timestamp": "2025-01-15T10:30:00.123Z",
"level": "info",
"message": "Publishing event",
"component": "sentinel",
"version": "v1.2.3",
"hostname": "sentinel-7d4b8c6f5",
"subset": "clusters",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"cluster_id": "cls-123"
}Error with stack trace and request context:
{
"timestamp": "2025-01-15T10:30:05.456Z",
"level": "error",
"message": "Unexpected error",
"component": "sentinel",
"version": "v1.2.3",
"hostname": "sentinel-7d4b8c6f5",
"subset": "clusters",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"cluster_id": "cls-123",
"error": "nil pointer dereference",
"stack_trace": [
"main.processCluster() processor.go:89",
"main.reconcileLoop() loop.go:45"
],
"request_context": {
"resource_generation": 5,
"last_observed_generation": 4
}
}Additional fields per component:
| Field | Description |
|---|---|
method |
HTTP method |
path |
Request path |
status_code |
Response status |
duration_ms |
Request duration |
user_agent |
Client user agent |
| Field | Description |
|---|---|
decision_reason |
Why event was published (generation_mismatch, max_age_expired) |
topic |
Pub/Sub topic name |
subset |
Resource subset identifier (e.g., clusters, nodepools) |
Note: Use
component=sentinelwithsubsetto identify specific instances. This allows filtering all Sentinels (WHERE component='sentinel') or a specific subset (WHERE component='sentinel' AND subset='clusters').
| Field | Description |
|---|---|
adapter |
Adapter type name |
job_result |
Outcome (success, failed, skipped) |
observed_generation |
Resource generation processed |
subscription |
Pub/Sub subscription name |
Components MUST propagate OpenTelemetry trace context:
- Incoming: Extract
trace_id/span_idfrom W3C headers (traceparent) - Outgoing: Inject trace headers when calling other services
- Events: Include
trace_idin CloudEvents - Logs: Always include
trace_idwhen available
This enables log correlation across: API → Sentinel → Broker → Adapters
The following MUST be redacted or omitted:
- API tokens and credentials
- Passwords and secrets
- Cloud provider access keys
- Personal identifiable information (PII)
To prevent truncation by log aggregators and control storage costs:
| Element | Recommendation |
|---|---|
| Message | Keep under 1 KB |
| Stack trace | Limit to 10-15 frames |
| Total entry | Keep under 64 KB |
Best practices:
- Log resource IDs, not full payloads (use
cluster_id, not the entire spec) - Truncate long strings with
...indicator - For debugging, log full payloads at
debuglevel only - Avoid logging large binary data or base64-encoded content
Note: Most log aggregation platforms (Cloud Logging, CloudWatch, Splunk) have limits between 64 KB and 256 KB per entry. Keeping entries under 64 KB ensures compatibility across platforms.