Cloud Logging - ericfitz/tmi GitHub Wiki

Cloud Logging

TMI's logger (internal/slogging) has a provider-neutral extension point for shipping logs to a cloud service. No cloud log provider is currently implemented (the former OCI writer was removed). The server starts with no CloudWriter, so cloud logging is off; logs go to the console and rotating files as described in Monitoring-and-Health.

AWS deployments

AWS deployments do not use CloudLogWriter. The terraform/modules/logging/aws module (used by aws-public) creates a CloudWatch log group (/tmi/<name_prefix>, configurable retention) and a Fluent Bit DaemonSet with an IRSA role that collects pod logs and ships them to CloudWatch. The module outputs the name as log_group_name; aws-public re-exports it as cloudwatch_log_group. See Terraform-Deployment.

The CloudLogWriter interface

Defined in internal/slogging/cloud_writer.go:

type CloudLogWriter interface {
    io.Writer
    WriteLog(ctx context.Context, entry LogEntry) error // structured entry (preferred)
    Flush(ctx context.Context) error
    Close() error
    Name() string
    IsHealthy(ctx context.Context) bool
}

type LogEntry struct {
    Timestamp time.Time
    Level     slog.Level
    Message   string
    Attrs     map[string]any
    Source    string // file:line if available
}

Implementations own their buffering, batching, and retry. NoopCloudWriter is a built-in no-op implementation for tests or when disabled.

Logger integration

LoggerConfig (in logger.go) has these optional cloud fields:

Field Meaning
CloudWriter The provider; nil disables cloud logging
CloudLogLevel Minimum level sent to the cloud (defaults to Level)
CloudLogBufferSize Async buffer size (default 1000)
CloudErrorThreshold Write-failure count that triggers a watchdog warning (default 5; config key logging.cloud_error_threshold, env TMI_LOG_CLOUD_ERROR_THRESHOLD)

When a CloudWriter is set, the logger wraps its handler in a CloudLogHandler. Each record is written to the local handler first, then converted to a LogEntry and queued on a buffered channel drained by a background goroutine that calls WriteLog. If the buffer is full the entry is written synchronously. Write errors are recorded (LastError(), ErrorCount(), Logger.CloudLogErrors()) and never fail local logging.

Graceful shutdown

Logger.Close() stops the watchdog, then closes the CloudLogHandler, which drains the buffer, calls Flush (10-second timeout), and then Close on the writer.

Watchdog and health

When a writer is configured, a watchdog polls it periodically and logs (via the local logger):

  • health transitions: cloud log sink healthy / cloud log sink unhealthy (from IsHealthy, 5-second timeout, panics treated as unhealthy);
  • cloud log sink failing writes once the failure count reaches CloudErrorThreshold, and cloud log sink writes recovered afterwards. A threshold of 0 disables the failure alarm but keeps health transitions.

Adding a provider

  1. Implement CloudLogWriter in internal/slogging (or a new package), including internal batching, retry, and a cheap IsHealthy.
  2. In cmd/server/main.go, where the logger is initialized (the code currently leaves CloudWriter/CloudLogLevel unset), construct your writer and set CloudWriter (and optionally CloudLogLevel) on the logger config.
  3. Add any provider configuration to internal/config and document it in Configuration-Reference.