zoobzio December 12, 2025 Edit this page

Logs Guide

Transform capitan events into OTEL logs.

Default Behavior

With no log configuration, all events are logged:

// No log config = log everything
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{...},
    // Logs: nil - all events logged
}
ap.Apply(schema)

sig1 := capitan.NewSignal("order.created", "Order created")
sig2 := capitan.NewSignal("order.shipped", "Order shipped")

cap.Emit(ctx, sig1)  // Logged
cap.Emit(ctx, sig2)  // Logged

Whitelist Filtering

Log only specific signals:

orderCreated := capitan.NewSignal("order.created", "Order created")
orderFailed := capitan.NewSignal("order.failed", "Order failed")
orderShipped := capitan.NewSignal("order.shipped", "Order shipped")

schema := aperture.Schema{
    Logs: &aperture.LogSchema{
        Whitelist: []string{
            "order.created",
            "order.failed",
        },
    },
}

cap.Emit(ctx, orderCreated)  // Logged
cap.Emit(ctx, orderFailed)   // Logged
cap.Emit(ctx, orderShipped)  // NOT logged (not in whitelist)

Log Attributes

Event fields become log attributes:

orderCreated := capitan.NewSignal("order.created", "Order created")
orderID := capitan.NewStringKey("order_id")
total := capitan.NewFloat64Key("total")
items := capitan.NewIntKey("items")

cap.Emit(ctx, orderCreated,
    orderID.Field("ORD-123"),
    total.Field(99.99),
    items.Field(3),
)

Produces log record with attributes:

signal="order.created"
order_id="ORD-123"
total=99.99
items=3

Signal Metadata

Every log record includes standard attributes:

AttributeSourceDescription
capitan.signalEvent.Signal()Signal name
capitan.signal.descriptionSignal descriptionSignal description
TimestampEvent.Timestamp()Event timestamp
SeverityEvent.Severity()Capitan severity level

Severity Mapping

Capitan severity maps to OTEL log severity:

CapitanOTEL
DebugDEBUG
InfoINFO
WarnWARN
ErrorERROR
FatalFATAL

Context Extraction for Logs

Add context values as log attributes:

type ctxKey string
const (
    userIDKey  ctxKey = "user_id"
    requestKey ctxKey = "request_id"
)

ap.RegisterContextKey("user_id", userIDKey)
ap.RegisterContextKey("request_id", requestKey)

schema := aperture.Schema{
    Context: &aperture.ContextSchema{
        Logs: []string{"user_id", "request_id"},
    },
}
ap.Apply(schema)

ctx = context.WithValue(ctx, userIDKey, "user-123")
ctx = context.WithValue(ctx, requestKey, "req-456")

cap.Emit(ctx, sig)
// Log includes: user_id="user-123", request_id="req-456"

Stdout Logging

Enable console logging alongside OTEL:

schema := aperture.Schema{
    Stdout: true,
}

cap.Emit(ctx, sig)
// Logs to both OTEL and stdout

Output format:

time=2025-01-15T10:30:00-08:00 level=INFO msg="Order created" signal=order.created order_id=ORD-123

Custom Type Handling

Custom types are automatically JSON serialized:

type OrderInfo struct {
    ID     string  `json:"id"`
    Total  float64 `json:"total"`
    Secret string  `json:"-"`  // Excluded from JSON
}

orderKey := capitan.NewKey[OrderInfo]("order", "Order details")

cap.Emit(ctx, sig, orderKey.Field(OrderInfo{
    ID:     "ORD-123",
    Total:  99.99,
    Secret: "xxx",
}))
// Log includes: order="{\"id\":\"ORD-123\",\"total\":99.99}"
// Secret excluded via json:"-" tag

Use JSON struct tags to control what gets exported.

Using Logger Directly

Access the underlying OTEL logger:

logger := ap.Logger("orders")

// Direct log emission
var record log.Record
record.SetBody(log.StringValue("Direct log message"))
record.SetSeverity(log.SeverityInfo)
record.AddAttributes(log.String("order_id", "ORD-123"))

logger.Emit(ctx, record)

Built-in Field Types

All capitan field types are transformed:

Capitan TypeLog Attribute
stringlog.String(key, value)
int, int32, int64log.Int64(key, value)
uint, uint32, uint64log.Int64(key, value)
float32, float64log.Float64(key, value)
boollog.Bool(key, value)
time.Timelog.String(key, rfc3339)
time.Durationlog.Int64(key, millis)
[]bytelog.Bytes(key, value)
Custom typeslog.String(key, json)

Schema Configuration

Via YAML:

logs:
  whitelist:
    - order.created
    - order.failed
    - payment.failed

stdout: true

Load and apply:

configBytes, _ := os.ReadFile("config.yaml")
schema, _ := aperture.LoadSchemaFromYAML(configBytes)
ap.Apply(schema)

Performance Considerations

  • Whitelist filtering happens before transformation (fast path for filtered events)
  • Field transformation is lazy (only when logging)
  • Stdout logging adds overhead; disable in production if not needed