Concepts
The Bridge Model
Aperture bridges two systems:
Capitan - In-process event coordination. Signals, fields, observers.
OpenTelemetry - Vendor-neutral observability. Logs, metrics, traces.
┌─────────────────┐ ┌─────────────────┐
│ Capitan │ │ OpenTelemetry │
│ │ │ │
│ Signals │──────────│ Logs │
│ Fields │ Aperture│ Metrics │
│ Events │──────────│ Traces │
│ │ │ │
└─────────────────┘ └─────────────────┘
Configuration vs Providers
Two separate concerns:
Provider Configuration (Your Responsibility)
How OTEL talks to backends:
// Exporter: where data goes
exporter, _ := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint("jaeger:4318"))
// Provider: how it's processed
traceProvider := trace.NewTracerProvider(
trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter)),
trace.WithSampler(trace.TraceIDRatioBased(0.1)),
trace.WithResource(myResource),
)
Schema Configuration (Transformation Rules)
How events become signals:
schema := aperture.Schema{
Metrics: []aperture.MetricSchema{...},
Traces: []aperture.TraceSchema{...},
Logs: &aperture.LogSchema{...},
}
Field Transformation
Capitan fields become OTEL attributes. This happens automatically for built-in types:
orderID := capitan.NewStringKey("order_id")
total := capitan.NewFloat64Key("total")
cap.Emit(ctx, sig, orderID.Field("ORD-123"), total.Field(99.99))
Becomes:
log.String("order_id", "ORD-123")
log.Float64("total", 99.99)
Custom Types
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, orderCreated, orderKey.Field(OrderInfo{
ID: "ORD-123",
Total: 99.99,
Secret: "internal",
}))
// Becomes: log.String("order", "{\"id\":\"ORD-123\",\"total\":99.99}")
Use standard JSON struct tags to control what gets exported.
Signal Types
Logs
Events become log records. By default, all events are logged. Use a whitelist to filter:
schema := aperture.Schema{
Logs: &aperture.LogSchema{
Whitelist: []string{"order.created", "order.failed"}, // Only these are logged
},
}
Metrics
Signals trigger metric operations. Four instrument types:
| Type | Use Case | Value Source |
|---|---|---|
| Counter | Count occurrences | Event emission |
| Gauge | Current value | Field value |
| Histogram | Distribution | Field value |
| UpDownCounter | Bidirectional count | Field value |
schema := aperture.Schema{
Metrics: []aperture.MetricSchema{
{Signal: "order.created", Name: "orders_total", Type: "counter"},
{Signal: "system.cpu", Name: "cpu_percent", Type: "gauge", ValueKey: "percent"},
},
}
Traces
Signal pairs become spans. Start and end events correlate by a key value:
schema := aperture.Schema{
Traces: []aperture.TraceSchema{
{
Start: "request.started",
End: "request.completed",
CorrelationKey: "request_id",
SpanName: "http_request",
},
},
}
// Events correlate by request_id value
cap.Emit(ctx, reqStarted, requestID.Field("REQ-123"))
cap.Emit(ctx, reqCompleted, requestID.Field("REQ-123")) // Completes span
Name-Based Matching
Schema configuration uses string names that match at runtime:
// Define signals with names
orderCreated := capitan.NewSignal("order.created", "Order created")
orderID := capitan.NewStringKey("order_id")
// Schema references by name
schema := aperture.Schema{
Metrics: []aperture.MetricSchema{
{Signal: "order.created", Name: "orders_total", Type: "counter"},
},
Traces: []aperture.TraceSchema{
{
Start: "order.created",
End: "order.completed",
CorrelationKey: "order_id",
},
},
}
// At runtime, event.Signal().Name() is compared to schema signal names
This enables hot-reload without recompilation.
Context Extraction
Pull values from context.Context into attributes:
type ctxKey string
const userIDKey ctxKey = "user_id"
// Register the context key
ap.RegisterContextKey("user_id", userIDKey)
// Reference by name in schema
schema := aperture.Schema{
Context: &aperture.ContextSchema{
Logs: []string{"user_id"},
},
}
ap.Apply(schema)
ctx = context.WithValue(ctx, userIDKey, "user-123")
cap.Emit(ctx, sig)
// ^ Log includes user_id="user-123" attribute
Schema-Based Configuration
Load configuration from files instead of code:
# observability.yaml
metrics:
- signal: order.created
name: orders_total
type: counter
logs:
whitelist:
- order.created
configBytes, _ := os.ReadFile("observability.yaml")
schema, _ := aperture.LoadSchemaFromYAML(configBytes)
ap.Apply(schema)
Enables hot-reload without recompilation.
Lifecycle
- Create providers - Configure OTEL exporters and processors
- Create aperture - Pass providers
- Apply schema - Configure transformation rules
- Emit events - Capitan events automatically transform
- Close aperture - Stop observing (does NOT shutdown providers)
- Shutdown providers - Your responsibility
// 1. Providers
logProvider := log.NewLoggerProvider(...)
defer logProvider.Shutdown(ctx) // 6. Your shutdown
// 2. Aperture
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
defer ap.Close() // 5. Stop observing
// 3. Schema
ap.Apply(schema)
// 4. Events
cap.Emit(ctx, sig, fields...)
Next Steps
- Architecture - Implementation details
- Metrics Guide - Deep dive on metrics
- Traces Guide - Deep dive on traces