otel_metrics.goā¢2.95 kB
package telemetry
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
const (
labelMCPServerName = "mcp_server_name"
labelToolName = "tool_name"
labelToolCallOutcome = "outcome"
)
const (
// attrValueMaxLen is the maximum length for string attribute values.
attrValueMaxLen = 64
attrValueUnknown = "unknown"
)
// OtelCustomMetrics bundles all the OpenTelemetry metric instruments used for MCPJungle.
// It implements the CustomMetrics interface.
type OtelCustomMetrics struct {
toolCalls metric.Int64Counter
toolCallLatency metric.Float64Histogram
}
// NewOtelCustomMetrics initializes all metric instruments required by MCPJungle.
// Returns an CustomMetrics instance ready for use, or an error if any instrument
// could not be created.
func NewOtelCustomMetrics(meter metric.Meter) (CustomMetrics, error) {
if meter == nil {
return nil, fmt.Errorf("meter cannot be nil")
}
toolInv, err := meter.Int64Counter(
"mcpjungle_tool_calls_total",
metric.WithDescription("Total number of tool calls"),
metric.WithUnit("1"),
)
if err != nil {
return nil, fmt.Errorf("failed to create tool calls counter: %w", err)
}
toolLat, err := meter.Float64Histogram(
"mcpjungle_tool_call_latency_seconds",
metric.WithDescription("Latency of tool calls in seconds"),
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30),
)
if err != nil {
return nil, fmt.Errorf("failed to create tool latency histogram: %w", err)
}
return &OtelCustomMetrics{
toolCalls: toolInv,
toolCallLatency: toolLat,
}, nil
}
func (m *OtelCustomMetrics) RecordToolCall(
ctx context.Context, mcpServerName, toolName string, outcome ToolCallOutcome, elapsedTime time.Duration,
) {
attrs := []attribute.KeyValue{
attribute.String(labelMCPServerName, boundString(mcpServerName)),
attribute.String(labelToolName, boundString(toolName)),
attribute.String(labelToolCallOutcome, string(outcome)),
}
m.toolCalls.Add(ctx, 1, metric.WithAttributes(attrs...))
m.toolCallLatency.Record(ctx, elapsedTime.Seconds(), metric.WithAttributes(attrs...))
}
func (m *OtelCustomMetrics) RecordPromptCall(
ctx context.Context, mcpServerName, promptName string, outcome PromptCallOutcome, elapsedTime time.Duration,
) {
attrs := []attribute.KeyValue{
attribute.String(labelMCPServerName, boundString(mcpServerName)),
attribute.String(labelToolName, boundString(promptName)),
attribute.String(labelToolCallOutcome, string(outcome)),
}
m.toolCalls.Add(ctx, 1, metric.WithAttributes(attrs...))
m.toolCallLatency.Record(ctx, elapsedTime.Seconds(), metric.WithAttributes(attrs...))
}
// boundString ensures strings are capped at maxLen and not empty.
func boundString(s string) string {
if s == "" {
return attrValueUnknown
}
if len(s) > attrValueMaxLen {
return s[:attrValueMaxLen]
}
return s
}