Overview
Connecting application events to observability platforms usually means manual instrumentation scattered throughout your code.
Aperture offers a declarative alternative: configure how capitan events become OpenTelemetry signals, and the bridge handles the rest.
// Define signals and keys
orderCreated := capitan.NewSignal("order.created", "Order created")
orderID := capitan.NewStringKey("order_id")
// Create aperture with your OTEL providers
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
defer ap.Close()
// Configure the bridge
schema := aperture.Schema{
Metrics: []aperture.MetricSchema{
{Signal: "order.created", Name: "orders_created_total", Type: "counter"},
},
}
ap.Apply(schema)
// Events automatically become OTEL signals
cap.Emit(ctx, orderCreated, orderID.Field("ORDER-123"))
// ^ Logged to OTEL + counter incremented
Schema-driven, hot-reloadable, explicit provider configuration.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Aperture │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Observer │ │
│ │ (receives all capitan events) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Logger │ │ Metrics │ │ Traces │ │
│ │ Transform │ │ Transform │ │ Transform │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ OTEL │ │ OTEL │ │ OTEL │ │
│ │ LogProvider│ │MeterProvider│ │TraceProvider│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Aperture registers as a capitan observer, receives all events, and transforms them according to configuration. You provide pre-configured OTEL providers; aperture handles the event-to-signal mapping.
Philosophy
Aperture draws a clear line: opinionated about event transformation, agnostic about provider configuration.
What aperture decides:
- How capitan fields become OTEL attributes
- How signal pairs correlate into spans
- How events filter to logs
What you decide:
- Which exporters to use (OTLP, stdout, custom)
- How to batch and buffer
- Security, sampling, resource attributes
// Your configuration choices
logProvider := log.NewLoggerProvider(
log.WithResource(myResource),
log.WithProcessor(log.NewBatchProcessor(myExporter)),
)
// Aperture's transformation rules
schema := aperture.Schema{
Metrics: []aperture.MetricSchema{...},
Traces: []aperture.TraceSchema{...},
Logs: &aperture.LogSchema{...},
}
// Two concerns, cleanly separated
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
ap.Apply(schema)
Capabilities
Configuration drives behavior:
Metrics - Count events, record values, track distributions. Signal emissions become counter increments, gauge updates, or histogram observations.
Traces - Correlate signal pairs into spans. request.started and request.completed with matching request IDs create a single span.
Logs - Filter events to logs. Whitelist specific signals or log everything.
Context - Extract values from context.Context and add them as attributes to all three signal types.
Schema - Load configuration from YAML/JSON files with hot-reload support.
Priorities
Explicit Configuration
No magic defaults that might expose data unexpectedly. You construct providers explicitly, you configure transformations explicitly.
// You control the exporter
exporter, _ := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint("localhost:4318"))
// You control the provider
traceProvider := trace.NewTracerProvider(
trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter)),
trace.WithSampler(trace.TraceIDRatioBased(0.1)),
)
// You pass it to aperture
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
Automatic Type Handling
Field transformation preserves types. Built-in types convert directly to OTEL attributes; 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",
}))
// OrderInfo serialized as JSON: {"id":"ORD-123","total":99.99}
Use standard JSON struct tags to control what gets exported.
OTEL Native
Aperture exposes standard OTEL interfaces. Use them directly when you need to:
logger := ap.Logger("orders")
meter := ap.Meter("orders")
tracer := ap.Tracer("orders")
// Standard OTEL usage
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
The bridge adds automatic event transformation; it doesn't replace direct OTEL usage.