-
Notifications
You must be signed in to change notification settings - Fork 0
Instrumentation
The controller framework provides comprehensive instrumentation capabilities for observability, including logging, tracing, and metrics collection. This system allows you to monitor your controllers' behavior, debug issues, and gain insights into performance characteristics.
Instrumentation can be used without using the full controller framework, making it a flexible choice for various applications. It does not rely on the framework's context nor generic types.
The instrumentation system is built around the Instrumenter interface, which provides:
- Structured logging with contextual information
- Distributed tracing for request flow visualization
- Context management for request-scoped data
- Integration with popular observability tools
The Instrumenter interface defines the contract for instrumentation providers:
type Instrumenter interface {
InstrumentRequestHandler(handler handler.TypedEventHandler[client.Object, reconcile.Request]) handler.TypedEventHandler[client.Object, reconcile.Request]
GetContextForRequest(req reconcile.Request) (*context.Context, bool)
GetContextForEvent(event any) *context.Context
NewQueue(mgr ctrl.Manager) func(controllerName string, rateLimiter workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request]
Cleanup(ctx *context.Context, req reconcile.Request)
NewLogger(ctx context.Context) logr.Logger
Tracer
}
type Tracer interface {
StartSpan(globalCtx *context.Context, localCtx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span)
}The InstrumentedReconciler wraps your reconciler with instrumentation capabilities:
type InstrumentedReconciler struct {
internalReconciler reconcile.TypedReconciler[reconcile.Request]
Instrumenter
}- Create an Instrumenter: Build an instrumenter with your desired configuration
-
Wrap your reconciler: Use
InstrumentedControllerManagedByto create an instrumented controller - Configure observability backends: Set up your logging and tracing providers
The framework provides a builder pattern for easy configuration:
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(loggerFunc).
WithTracer(tracer).
Build()Sentry provides both error tracking and performance monitoring through logging and tracing.
Add Sentry dependencies to your go.mod:
go get github.com/getsentry/sentry-go
go get github.com/getsentry/sentry-go/otelpackage main
import (
"github.com/getsentry/sentry-go"
sentryotel "github.com/getsentry/sentry-go/otel"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/u-ctf/controller-fwk/instrument"
)
func main() {
// Initialize Sentry
sentry.Init(sentry.ClientOptions{
Dsn: "https://your-sentry-dsn@sentry.io/project-id",
EnableTracing: true,
TracesSampleRate: 1.0, // Adjust sampling rate for production
Environment: "production", // or "development", "staging"
Release: "v1.0.0",
})
// Set up OpenTelemetry with Sentry
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sentryotel.NewSentrySpanProcessor()),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(sentryotel.NewSentryPropagator())
// Create instrumenter with Sentry logger and tracer
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(instrument.NewSentryLoggerFunc(ctrl.Log.WithName("controller"))).
WithTracer(instrument.NewSentryTracer(tp.Tracer("controller"))).
Build()
// Use in your reconciler setup
if err := (&YourReconciler{
Client: mgr.GetClient(),
Instrumenter: instrumenter,
EventRecorder: mgr.GetEventRecorderFor("your-controller"),
RuntimeScheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
}EnableTracing: false), you still need to configure both the logger and tracer components for the instrumentation to work correctly.
// ✅ Correct - Use both logger and tracer together
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(instrument.NewSentryLoggerFunc(baseLogger)).
WithTracer(instrument.NewSentryTracer(tracer)).
Build()
// ❌ Incorrect - Don't use only one component
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(instrument.NewSentryLoggerFunc(baseLogger)).
// Missing tracer - will cause issues
Build()With Sentry integration, you get:
- Error Tracking: Automatic capture of reconciliation errors with stack traces
- Performance Monitoring: Trace reconciliation performance across requests
- Release Tracking: Link errors and performance to specific releases
- Context: Rich context including Kubernetes resource information
Jaeger provides distributed tracing capabilities for visualizing request flows.
Add OpenTelemetry Jaeger dependencies:
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/exporters/jaeger
go get go.opentelemetry.io/otel/sdk/tracepackage main
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"github.com/u-ctf/controller-fwk/instrument"
)
func setupJaeger() (*sdktrace.TracerProvider, error) {
// Create Jaeger exporter
jaegerExporter, err := jaeger.New(
jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://localhost:14268/api/traces"),
),
)
if err != nil {
return nil, err
}
// Create resource with service information
res, err := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceNameKey.String("my-controller"),
semconv.ServiceVersionKey.String("v1.0.0"),
semconv.DeploymentEnvironmentKey.String("production"),
),
)
if err != nil {
return nil, err
}
// Create tracer provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(jaegerExporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()), // Adjust for production
)
return tp, nil
}
func main() {
// Set up Jaeger tracing
tp, err := setupJaeger()
if err != nil {
setupLog.Error(err, "failed to setup Jaeger")
os.Exit(1)
}
defer func() { _ = tp.Shutdown(context.Background()) }()
otel.SetTracerProvider(tp)
// Create instrumenter with Jaeger tracer
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(func(ctx context.Context) logr.Logger {
return ctrl.Log.WithName("controller")
}).
WithTracer(instrument.NewOtelTracer(tp.Tracer("controller"))).
Build()
// Use in your reconciler
if err := (&YourReconciler{
Client: mgr.GetClient(),
Instrumenter: instrumenter,
EventRecorder: mgr.GetEventRecorderFor("your-controller"),
RuntimeScheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
}With Jaeger integration, you get:
- Distributed Tracing: Visualize request flows across services
- Performance Analysis: Identify bottlenecks and latency issues
- Service Dependencies: Understand service interaction patterns
- Custom Spans: Add custom instrumentation points
Replace the standard controller builder with the instrumented version:
func (r *YourReconciler) SetupWithManager(mgr ctrl.Manager) error {
return instrument.InstrumentedControllerManagedBy(r.Instrumenter, mgr).
For(&yourv1.YourResource{}).
Named("your-controller").
Build(r)
}Your reconciler can access instrumentation through the embedded Instrumenter:
type YourReconciler struct {
client.Client
instrument.Instrumenter
record.EventRecorder
RuntimeScheme *runtime.Scheme
// ... other fields
}
func (r *YourReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Instrumentation is automatically applied by InstrumentedReconciler
// The logger is automatically enhanced with tracing context
// Your reconciliation logic here
// Logger is available via logf.FromContext(ctx)
return ctrl.Result{}, nil
}Create custom logger functions for specific needs:
func customLoggerFunc(ctx context.Context) logr.Logger {
baseLogger := ctrl.Log.WithName("my-controller")
// Add custom context information
if traceID := getTraceIDFromContext(ctx); traceID != "" {
baseLogger = baseLogger.WithValues("traceId", traceID)
}
return baseLogger
}
instrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(customLoggerFunc).
Build()You can create different instrumenters for different controllers:
// High-volume controller with sampling
highVolumeInstrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(instrument.NewSentryLoggerFunc(ctrl.Log)).
WithTracer(instrument.NewSentryTracer(sampledTracer)).
Build()
// Critical controller with full tracing
criticalInstrumenter := instrument.NewInstrumenter(mgr).
WithLoggerFunc(instrument.NewSentryLoggerFunc(ctrl.Log)).
WithTracer(instrument.NewSentryTracer(fullTracer)).
Build()- Sampling: Use appropriate sampling rates for high-volume environments
- Context Cleanup: The framework automatically handles context cleanup
- Resource Management: Instrumenters manage their own resources
- Structured Errors: Use structured logging for better error analysis
- Context Preservation: Maintain context across async operations
- Graceful Degradation: Handle instrumentation failures gracefully
- Sensitive Data: Avoid logging sensitive information
- Network Security: Secure connections to observability backends
- Authentication: Use proper authentication for observability services
- Missing Traces: Ensure tracer provider is properly configured
- Context Loss: Verify context is properly propagated
- High Memory Usage: Check sampling rates and batch sizes
Enable debug logging to troubleshoot instrumentation issues:
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))If you're currently using manual instrumentation:
- Remove manual logger and tracer setup from reconcilers
- Configure instrumenter at the manager level
- Use
InstrumentedControllerManagedByinstead of standard builder - Update reconciler struct to embed
instrument.Instrumenter - Pass the instrumenter as the first argument to
InstrumentedControllerManagedBy
The instrumentation system is compatible with:
- controller-runtime v0.15+
- OpenTelemetry Go v1.19+
- Go 1.19+