Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ task license-fix # Add missing license headers
| `mcpcompat/mcp` | Drop-in shim for `mark3labs/mcp-go/mcp` data types; re-exports via aliases, backed by go-sdk elsewhere (Alpha) |
| `mcpcompat/client` | Drop-in shim for `mark3labs/mcp-go/client` (+ `client/transport`) reimplemented on the official go-sdk (Alpha) |
| `mcpcompat/server` | Drop-in shim for `mark3labs/mcp-go/server` reimplemented on the official go-sdk (Alpha) |
| `metrics` | Shared OTel histogram bucket presets and label-key constants (Alpha) |
| `telemetry/metrics` | Shared OTel histogram bucket presets, label-key constants, and emitter-ownership vocabulary (Alpha) |
| `telemetry/reconcile` | Unified operator reconcile metric emitter (Alpha) |
| `oci/artifact` | Artifact-agnostic OCI tar/gzip/extraction/platform primitives shared by oci/skills and oci/plugins (Alpha) |
| `oci/skills` | OCI artifact types, media types, and registry operations for ToolHive skills (Alpha) |
| `oci/plugins` | OCI artifact types, media types, and registry operations for ToolHive plugins (Alpha) |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ The ToolHive ecosystem spans multiple Go repositories, and several of these proj
| `env` | Stable | Environment variable abstraction with `Reader` interface |
| `httperr` | Stable | Wrap errors with HTTP status codes |
| `logging` | Alpha | Pre-configured `*slog.Logger` factory with consistent ToolHive defaults |
| `metrics` | Alpha | Shared OTel histogram bucket presets and label-key constants |
| `telemetry/metrics` | Alpha | Shared OTel histogram bucket presets, label-key constants, and emitter-ownership vocabulary |
| `telemetry/reconcile` | Alpha | Unified operator reconcile metric emitter |
| `oci/skills` | Alpha | OCI artifact types, media types, and registry operations for skills |
| `oci/plugins` | Alpha | OCI artifact types, media types, and registry operations for plugins |
| `postgres` | Alpha | PostgreSQL connection pool with optional AWS RDS IAM dynamic auth |
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ require (
github.com/sigstore/sigstore-go v1.2.2
github.com/stretchr/testify v1.11.1
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.uber.org/mock v0.6.0
golang.org/x/net v0.57.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -107,8 +110,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
Expand All @@ -122,7 +124,7 @@ require (
golang.org/x/text v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/grpc v1.82.0 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,8 @@ go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
Expand Down Expand Up @@ -426,8 +428,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
46 changes: 0 additions & 46 deletions metrics/doc.go

This file was deleted.

75 changes: 0 additions & 75 deletions metrics/metrics_test.go

This file was deleted.

65 changes: 65 additions & 0 deletions telemetry/metrics/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package metrics

import (
"context"
"fmt"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

// BuildInfoMetricName is the fleet-wide build/release-correlation gauge. It is
// the one Stacklok-authored metric with no <service> segment (RFC §3.4), shared
// verbatim by every component.
const BuildInfoMetricName = "stacklok.build_info"

// unknownBuildInfoValue is the fallback for an empty version or commit.
const unknownBuildInfoValue = "unknown"

// RegisterBuildInfo registers the stacklok.build_info observable gauge on the
// given meter. The gauge always observes 1; the identity rides its labels
// (component, version, commit). Empty version/commit fall back to unknownBuildInfoValue.
//
// The component label uses the bare "component" key, not the dotted
// stacklok.component resource attribute (AttrStacklokComponent), so it does not
// collide with the stacklok_component constant label the Prometheus exporter
// promotes from the resource (D8). Pass the caller's own component value (the
// same string it stamps as AttrStacklokComponent).
//
// As with the rest of this package, RegisterBuildInfo is a construction helper:
// the caller supplies the meter (typically otel.Meter(scope) after installing
// its provider). It registers no global state of its own.
func RegisterBuildInfo(meter metric.Meter, component, version, commit string) error {
if meter == nil {
return fmt.Errorf("metrics: meter must not be nil")
}
if component == "" {
return fmt.Errorf("metrics: component must not be empty")
}
if version == "" {
version = unknownBuildInfoValue
}
if commit == "" {
commit = unknownBuildInfoValue
}

_, err := meter.Int64ObservableGauge(BuildInfoMetricName,
metric.WithUnit("1"),
metric.WithDescription("Build information; always 1, identity carried on labels."),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(1, metric.WithAttributes(
attribute.String("component", component),
attribute.String("version", version),
attribute.String("commit", commit),
))
return nil
}),
)
if err != nil {
return fmt.Errorf("register build_info gauge: %w", err)
}
return nil
}
71 changes: 71 additions & 0 deletions telemetry/metrics/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

/*
Package metrics provides shared histogram bucket presets, label-key
constants, and emitter-ownership vocabulary for OpenTelemetry metrics across
the ToolHive ecosystem.

The section and decision numbers cited throughout this package (e.g. "RFC
§3.3", "D8") refer to the internal Stacklok Platform Metrics Standardization
RFC, which defines the canonical vocabulary this package pins.

This package is construction helpers and vocabulary only: importing it
installs no global providers and emits no metrics of its own. The one
exception is RegisterBuildInfo, which registers an observable gauge on the
meter the caller passes in — see "Build Info" below; it still constructs no
meter or provider itself. Consumers (the ToolHive proxy, vMCP, and the
registry) use the rest of this package to build their own instruments
consistently.

# Bucket Presets

Three histogram bucket presets cover fast HTTP calls, MCP/proxy operations,
and long-running operations such as sync, reconcile, or composite-tool
execution. This package does not construct meters or instruments; callers
import go.opentelemetry.io/otel and go.opentelemetry.io/otel/metric
themselves and pass a preset's boundaries when constructing a histogram:

meter := otel.Meter("stacklok.toolhive.proxy")
histogram, err := meter.Float64Histogram(
"stacklok.toolhive.proxy.request.duration",
metric.WithExplicitBucketBoundaries(metrics.BucketsFastHTTP()...),
)

# Label Keys

Canonical label-key constants ensure every consumer spells the same concept
the same way. As with the bucket presets, callers attach these to their own
OTel instruments via go.opentelemetry.io/otel/attribute:

counter.Add(ctx, 1, metric.WithAttributes(
attribute.String(metrics.LabelOutcome, "success"),
))

This package exports only canonical common keys: concepts more than one
component emits and that a cross-component dashboard joins or groups on.
Component-local keys used by a single emitter are not exported here; they
are defined by that component.

# Emitter Ownership

AttrStacklokComponent and AttrStacklokProduct are resource attributes (RFC
§3.3, D8), not metric labels: a component sets them once on its OTel resource,
and the Prometheus exporter promotes them to per-series labels via
WithResourceAsConstantLabels. ProductStacklokPlatform is the frozen value
every component stamps verbatim; the per-component stacklok.component value is
supplied by each component, not enumerated here.

# Build Info

RegisterBuildInfo registers the fleet-wide stacklok.build_info observable
gauge on a caller-provided meter, for release correlation across components:

err := metrics.RegisterBuildInfo(meter, "toolhive", version, commit)

# Stability

This package is Alpha stability. The API may change without notice.
See the toolhive-core README for stability level definitions.
*/
package metrics
19 changes: 19 additions & 0 deletions metrics/labels.go → telemetry/metrics/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,22 @@ const (
// metric (coexists with semconv error.type on OTel semconv metrics).
LabelErrorType = "error_type"
)

// Canonical outcome-label values. The LabelOutcome key carries one of these on
// a Stacklok-authored counter, distinguishing success from failure without a
// separate _succeed/_failed metric name (RFC §3.4 / D4). A metric may extend
// this set with a documented, bounded per-metric outcome where the standard
// three do not capture a distinct terminal state (e.g. the journal lifecycle
// outcomes, the vMCP optimizer's "not_found"); such extensions are owned by the
// emitting component, not exported here.
const (
// OutcomeSuccess marks a successful operation.
OutcomeSuccess = "success"

// OutcomeError marks a failed operation.
OutcomeError = "error"

// OutcomeRejected marks an operation refused before execution (e.g. rate
// limited, circuit open, admission denied).
OutcomeRejected = "rejected"
)
File renamed without changes.
Loading