Observability Developer Guide
This guide explains how to instrument your code with observability events and provides an overview of the observability architecture. It is intended for developers adding new features or components to Thunder.
Table of Contents​
Integration Guide​
Thunder uses a dependency injection pattern for observability. To instrument your component:
1. Inject the Observability Service​
Your component should accept ObservabilityServiceInterface in its constructor or initialization method.
import "github.com/asgardeo/thunder/internal/observability"
type MyComponent struct {
obsSvc observability.ObservabilityServiceInterface
}
func NewMyComponent(obsSvc observability.ObservabilityServiceInterface) *MyComponent {
return &MyComponent{
obsSvc: obsSvc,
}
}
2. Publish Events​
Use the injected service to publish events. Always check if the service is enabled (though the service handles no-ops, checking avoids unnecessary object creation).
import "github.com/asgardeo/thunder/internal/observability/event"
func (c *MyComponent) DoSomething(ctx context.Context) {
// 1. Check if enabled
if c.obsSvc == nil || !c.obsSvc.IsEnabled() {
return
}
// 2. Create event
traceID := uuid.NewString() // Or get from context
evt := event.NewEvent(traceID, event.Type.MyOperation, "MyComponent").
WithStatus(event.StatusSuccess).
WithData(event.DataKey.UserID, "user-123")
// 3. Publish
c.obsSvc.PublishEvent(evt)
}
Event Anatomy​
Required Fields​
- TraceID: UUID or hex string for trace correlation.
- EventType: Predefined constant from
event.Type(e.g.,event.Type.FlowStarted). - Component: Your component name (e.g.,
"AuthHandler","FlowEngine").
Optional Fields​
- Status:
event.StatusSuccess,event.StatusFailure, orevent.StatusInProgress. - Data: Key-value pairs using
event.DataKeyconstants.
Common Data Keys​
Always use predefined keys from event.DataKey for consistency:
| Key | Usage |
|---|---|
UserID | Authenticated user identifier |
ClientID | OAuth client identifier |
FlowID | Flow execution identifier |
Message | Human-readable message |
Error | Error message for failures |
LatencyUs | Operation latency in microseconds |
See event/datakeys.go for the complete list.
Distributed Tracing​
Events with the same TraceID are automatically grouped into a single trace by the OpenTelemetry subscriber.
Hierarchical Tracing​
To create parent-child relationships between spans, include the TraceParent key:
// Child operation
childEvt := event.NewEvent(traceID, event.Type.NodeExecutionStarted, "FlowEngine").
WithData(event.DataKey.TraceParent, parentSpanID)
obs.PublishEvent(childEvt)
Architecture Overview​
The Observability component follows a Publisher-Subscriber pattern, decoupled from core business logic.
Core Components​
- Service (
Service): The main entry point. Manages lifecycle and configuration. - Publisher (
CategoryPublisher): Acts as the event bus. Distributes events to subscribers based on categories. - Subscribers (
SubscriberInterface): Consume events (e.g., Console, File, OTel).
Directory Structure​
backend/internal/observability/
├── service.go # Main service implementation
├── event/ # Event definitions
├── publisher/ # Publisher implementation
├── subscriber/ # Subscriber implementations
└── opentelemetry/ # OpenTelemetry configuration
Extending Observability​
To add a new subscriber (e.g., to send logs to a webhook):
- Create a new file in
backend/internal/observability/subscriber/. - Implement
SubscriberInterface:type SubscriberInterface interface {
Initialize() error
IsEnabled() bool
GetID() string
GetCategories() []event.EventCategory
OnEvent(evt *event.Event) error
Close() error
} - Register the Factory:
Add an
init()function to register your subscriber factory.func init() {
RegisterSubscriberFactory("my-subscriber", func() SubscriberInterface {
return NewMySubscriber()
})
} - Add Configuration: Update
backend/internal/system/config/config.go.