Skip to content

Monitor Events

Learn how to observe SDK operations with the hooks package.

import (
"github.com/AltairaLabs/PromptKit/sdk/hooks"
"github.com/AltairaLabs/PromptKit/runtime/events"
)
hooks.On(conv, events.EventProviderCallStarted, func(e *events.Event) {
fmt.Printf("Provider call started\n")
})

Events are defined as events.EventType in the runtime/events package, grouped by category:

// Pipeline lifecycle
EventPipelineStarted EventType = "pipeline.started"
EventPipelineCompleted EventType = "pipeline.completed"
EventPipelineFailed EventType = "pipeline.failed"
// Middleware execution
EventMiddlewareStarted EventType = "middleware.started"
EventMiddlewareCompleted EventType = "middleware.completed"
EventMiddlewareFailed EventType = "middleware.failed"
// Stage execution (streaming pipeline)
EventStageStarted EventType = "stage.started"
EventStageCompleted EventType = "stage.completed"
EventStageFailed EventType = "stage.failed"
// Provider (LLM) calls
EventProviderCallStarted EventType = "provider.call.started"
EventProviderCallCompleted EventType = "provider.call.completed"
EventProviderCallFailed EventType = "provider.call.failed"
// Tool calls
EventToolCallStarted EventType = "tool.call.started"
EventToolCallCompleted EventType = "tool.call.completed"
EventToolCallFailed EventType = "tool.call.failed"
// Validation
EventValidationStarted EventType = "validation.started"
EventValidationPassed EventType = "validation.passed"
EventValidationFailed EventType = "validation.failed"
// Context & state
EventContextBuilt EventType = "context.built"
EventTokenBudgetExceeded EventType = "context.token_budget_exceeded"
EventContextCompacted EventType = "context.compacted"
EventStateLoaded EventType = "state.loaded"
EventStateSaved EventType = "state.saved"
// Messages & conversation
EventMessageCreated EventType = "message.created"
EventMessageUpdated EventType = "message.updated"
EventConversationStarted EventType = "conversation.started"
// Multimodal
EventAudioInput EventType = "audio.input"
EventAudioOutput EventType = "audio.output"
EventAudioTranscription EventType = "audio.transcription"
EventVideoFrame EventType = "video.frame"
EventScreenshot EventType = "screenshot"
EventImageInput EventType = "image.input"
EventImageOutput EventType = "image.output"
// Evals
EventEvalCompleted EventType = "eval.completed" // eval finished (any score)
EventEvalFailed EventType = "eval.failed" // eval errored (not low score)
// Client tool lifecycle
EventClientToolRequest EventType = "tool.client.request" // client tool awaiting fulfillment
EventClientToolResolved EventType = "tool.client.resolved" // client tool resolved by caller
// Stream control
EventStreamInterrupted EventType = "stream.interrupted"
hooks.OnToolCall(conv, func(name string, args map[string]any) {
fmt.Printf("Tool called: %s(%v)\n", name, args)
})
hooks.On(conv, events.EventValidationFailed, func(e *events.Event) {
data := e.Data.(*events.ValidationEventData)
log.Printf("Guardrail %s triggered: score=%.2f enforced=%v monitor=%v",
data.ValidatorName, data.Score, data.Enforced, data.MonitorOnly)
})

The ValidationEventData includes:

FieldDescription
ValidatorNameValidator type (e.g., banned_words, max_length)
ScoreEvaluation score (0.0–1.0, lower means more violation)
Enforcedtrue if content was modified (truncated/replaced)
MonitorOnlytrue if the guardrail evaluated without enforcing
ViolationsViolation details (reason strings)
DurationHow long the evaluation took
hooks.OnProviderCall(conv, func(model string, inputTokens, outputTokens int, cost float64) {
log.Printf("Model %s: %d in, %d out, $%.4f", model, inputTokens, outputTokens, cost)
})
func attachLogger(conv *sdk.Conversation) {
hooks.OnEvent(conv, func(e *events.Event) {
log.Printf("[%s] %s", e.Timestamp.Format("15:04:05"), e.Type)
})
}
// From runtime/events package
type Event struct {
Type EventType
Timestamp time.Time
RunID string
SessionID string
ConversationID string
Data EventData // Type-specific payload
}

WithMetrics() enables automatic Prometheus metrics for both pipeline operations and eval results. It follows the same pattern as WithTracerProvider() — pass a collector, and the SDK handles the rest.

import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/AltairaLabs/PromptKit/runtime/metrics"
"github.com/AltairaLabs/PromptKit/sdk"
)
// 1. Create collector once per process
reg := prometheus.NewRegistry()
collector := metrics.NewCollector(metrics.CollectorOpts{
Registerer: reg,
Namespace: "myapp",
ConstLabels: prometheus.Labels{"env": "prod"},
})
// 2. Attach to conversations
conv, _ := sdk.Open("./app.pack.json", "chat",
sdk.WithMetrics(collector, nil),
)
defer conv.Close()
// 3. Expose via your own HTTP server
http.Handle("/metrics", promhttp.HandlerFor(collector.Registry(), promhttp.HandlerOpts{}))

When multiple conversations share one Prometheus endpoint, use instance labels to distinguish them:

collector := metrics.NewCollector(metrics.CollectorOpts{
Registerer: reg,
Namespace: "myapp",
ConstLabels: prometheus.Labels{"env": "prod"},
InstanceLabels: []string{"tenant", "prompt_name"},
})
conv1, _ := sdk.Open(pack, "support", sdk.WithMetrics(collector, map[string]string{
"tenant": "acme", "prompt_name": "support",
}))
conv2, _ := sdk.Open(pack, "sales", sdk.WithMetrics(collector, map[string]string{
"tenant": "globex", "prompt_name": "sales",
}))

These are recorded automatically from EventBus events:

MetricTypeLabels
{ns}_pipeline_duration_secondshistogramstatus
{ns}_provider_request_duration_secondshistogramprovider, model
{ns}_provider_requests_totalcounterprovider, model, status
{ns}_provider_input_tokens_totalcounterprovider, model
{ns}_provider_output_tokens_totalcounterprovider, model
{ns}_provider_cached_tokens_totalcounterprovider, model
{ns}_provider_cost_totalcounterprovider, model
{ns}_tool_call_duration_secondshistogramtool
{ns}_tool_calls_totalcountertool, status
{ns}_validation_duration_secondshistogramvalidator, validator_type
{ns}_validations_totalcountervalidator, validator_type, status

Pack-defined eval metrics (from EvalDef.Metric) are also recorded through the same collector under the {ns}_eval_ sub-namespace. For example, the metric below becomes myapp_eval_response_relevance_score. No extra wiring needed — WithMetrics() handles both pipeline and eval metrics.

{
"evals": [
{
"id": "response_relevance",
"type": "llm_judge",
"trigger": "every_turn",
"metric": {
"name": "response_relevance_score",
"type": "gauge",
"labels": {
"eval_type": "llm_judge",
"category": "quality"
}
},
"params": {
"criteria": "Is the response relevant to the user's question?"
}
}
]
}
TypeBehavior
gaugeSet to the eval’s score value
counterIncrement on each eval execution
histogramObserve score with configurable buckets
booleanRecord 1.0 if score ≥ 1.0, 0.0 otherwise
FieldTypeDescription
Registererprometheus.RegistererRegistry to register into (default: DefaultRegisterer)
NamespacestringMetric name prefix (default: "promptkit")
ConstLabelsprometheus.LabelsProcess-level constant labels (env, region)
InstanceLabels[]stringLabel names that vary per conversation (tenant, prompt_name). Sorted internally — Bind() label order doesn’t matter.
DisablePipelineMetricsboolDisable operational metrics (use for eval-only consumers, or use NewEvalOnlyCollector)
DisableEvalMetricsboolDisable eval result metrics

For ad-hoc counters not covered by the built-in metrics, use hooks:

type Metrics struct {
ToolCalls int64
Errors int64
mu sync.Mutex
}
func (m *Metrics) Attach(conv *sdk.Conversation) {
hooks.On(conv, events.EventToolCallStarted, func(e *events.Event) {
m.mu.Lock()
m.ToolCalls++
m.mu.Unlock()
})
hooks.On(conv, events.EventToolCallFailed, func(e *events.Event) {
m.mu.Lock()
m.Errors++
m.mu.Unlock()
})
}
func enableDebug(conv *sdk.Conversation) {
hooks.OnEvent(conv, func(e *events.Event) {
log.Printf("[DEBUG] %s: %s", e.Timestamp.Format("15:04:05"), e.Type)
})
}
package main
import (
"context"
"fmt"
"log"
"github.com/AltairaLabs/PromptKit/sdk"
"github.com/AltairaLabs/PromptKit/sdk/hooks"
"github.com/AltairaLabs/PromptKit/runtime/events"
)
func main() {
conv, _ := sdk.Open("./app.pack.json", "chat")
defer conv.Close()
// Monitor all activity
hooks.OnEvent(conv, func(e *events.Event) {
log.Printf("[%s] %s", e.Timestamp.Format("15:04:05"), e.Type)
})
// Monitor tool calls specifically
hooks.OnToolCall(conv, func(name string, args map[string]any) {
log.Printf("Tool called: %s", name)
})
// Use normally
ctx := context.Background()
resp, _ := conv.Send(ctx, "Hello!")
fmt.Println(resp.Text())
}