httpmetrics

package
v1.58.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 50 Imported by: 14

README

HTTP metrics and serving revisions

This package provides HTTP metrics, tracing, and optional serving-revision metadata for Cloud Run services.

Serving-revision metadata

The revision helpers attach x-chainguard-revision to HTTP response headers or gRPC response metadata. Pass the process's K_REVISION value when constructing the handler or server. When the value is empty, no revision metadata is added; local development does not report a fabricated unknown revision.

HTTP services can compose the helper with their existing instrumentation:

handler := httpmetrics.Handler("api",
    httpmetrics.ServerRevisionHandler(os.Getenv("K_REVISION"), apiHandler))

For native gRPC servers, register the unary and streaming interceptors:

revision := os.Getenv("K_REVISION")
server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        authenticateUnary,
        httpmetrics.ServerRevisionUnaryInterceptor(revision),
    ),
    grpc.ChainStreamInterceptor(
        authenticateStream,
        httpmetrics.ServerRevisionStreamInterceptor(revision),
    ),
)

Authentication should run first if only authenticated responses should expose the revision. For HTTP, place authentication middleware outside ServerRevisionHandler. Install each helper once and reserve ServerRevisionHeader for it. The helpers are opt-in: existing metrics handlers, services, and clients retain their current behavior until explicitly wired up.

The header accompanies application errors as well as successful responses. A request rejected before reaching the helper, such as by Cloud Run IAM or an earlier authentication interceptor, may have no revision. If an earlier gRPC interceptor has already sent headers, the revision cannot be added; the application still runs and keeps its normal result. Revision metadata is best effort and never changes a gRPC application's result. Streaming gRPC queues the metadata for the normal header send, usually the first response or final status; it does not send an early response just to expose the revision. HTTP retains the original response writer and its streaming and hijacking capabilities.

HTTP clients read response.Header.Values(httpmetrics.ServerRevisionHeader). Unary gRPC clients pass grpc.Header(&headers) and read headers.Get(httpmetrics.ServerRevisionHeader); streaming clients use stream.Header(). A probe attributing a response to a revision should require exactly one non-empty value. Missing or multiple values are inconclusive. Treat the value as diagnostic data from the responding service, not as proof of identity or a credential.

Documentation

Overview

Package httpmetrics provides HTTP middleware and transport wrappers that instrument requests with Prometheus metrics, OpenTelemetry tracing, and structured logging for Cloud Run services. Optional HTTP and gRPC server wrappers identify the serving revision in response headers.

Index

Examples

Constants

View Source
const (
	DiskUsageScrapeInterval    = 5 * time.Second
	DiskUsageScrapeIntervalEnv = "DISK_USAGE_SCRAPE_INTERVAL"
)
View Source
const (
	CeTypeHeader          string = "ce-type"
	GoogClientTraceHeader string = "googclient_traceparent"
	OriginalTraceHeader   string = "original-traceparent"
)
View Source
const CacheResultHeader = "X-Httpmetrics-Cache-Result"

CacheResultHeader is how a response cache wrapped by this transport (that is, one between it and the network) says how it answered: "hit" for a replayed body GitHub confirmed with a 304, "changed" for a revalidation GitHub answered with a new body, "miss" for a request it had nothing cached for. Such a cache replays a 304 as the 200 it stands for, so this is the only way the github_api_call log can tell the two apart. The transport logs the value as the cache field and deletes the header, so callers above it never see it.

View Source
const ServerRevisionHeader = "x-chainguard-revision"

ServerRevisionHeader identifies the revision that served a response. It is shared by HTTP response headers and gRPC response metadata. The value is diagnostic information, not an authentication or authorization credential.

Variables

Transport is an http.RoundTripper that records metrics for each request.

Functions

func ExtractInnerTransport added in v0.5.156

func ExtractInnerTransport(rt http.RoundTripper) http.RoundTripper

ExtractInnerTransport recursively unwraps layers of RoundTripper wrapping (MetricsTransport and any TransportUnwrapper) to find the base transport. Stops after maxUnwrapDepth iterations to prevent infinite loops.

Example
package main

import (
	"net/http"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	wrapped := httpmetrics.WrapTransport(http.DefaultTransport)
	inner := httpmetrics.ExtractInnerTransport(wrapped)
	_ = inner
}

func GenerateRelatedTraceID added in v1.35.0

func GenerateRelatedTraceID(ctx context.Context) oteltrace.TraceID

GenerateRelatedTraceID returns a new trace id carrying the same GCP export decision as the trace in ctx, for re-rooting work without dangling links under sampling. Requires both services to run the same OTEL_TRACE_SAMPLING_RATE. Without a span in ctx the id is fully random. An all-zero (invalid) result is possible in principle; the SDK generates a fresh root id when an invalid id is planted, so no retry here.

Example
package main

import (
	"context"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
)

func main() {
	// Re-root work as its own trace whose export decision matches the
	// caller's: plant the related trace id (no span id) and the SDK adopts
	// it as a new root. Real servers receive the caller span context via
	// inbound propagation; the example plants one. See pkg/tracesplit for
	// the packaged version of this pattern.
	ctx := trace.ContextWithRemoteSpanContext(context.Background(),
		trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
			SpanID:     trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8},
			TraceFlags: trace.FlagsSampled,
			Remote:     true,
		}))

	// Derive and link only when there is a real caller trace.
	var opts []trace.SpanStartOption
	if caller := trace.SpanContextFromContext(ctx); caller.IsValid() {
		ctx = trace.ContextWithSpanContext(ctx, trace.NewSpanContext(trace.SpanContextConfig{
			TraceID: httpmetrics.GenerateRelatedTraceID(ctx),
		}))
		opts = append(opts, trace.WithLinks(trace.Link{SpanContext: caller}))
	}
	_, span := otel.Tracer("example").Start(ctx, "build", opts...)
	defer span.End()
}

func GitHubPathBucket added in v1.41.0

func GitHubPathBucket(path string) string

GitHubPathBucket maps a GitHub REST API path to its bounded-cardinality bucket (e.g. "/repos/{org}/{repo}/pulls/{number}"), the same bucket the instrumented transport stamps on github_api_call log lines. Paths matching no known endpoint collapse into "unknown_gh_path". It is exported so services that front the GitHub API can label their own metrics consistently.

func Handler

func Handler(name string, handler http.Handler) http.Handler

Handler wraps a given http handler in standard metrics handlers.

Example
package main

import (
	"net/http"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	h := httpmetrics.Handler("my-handler", inner)
	_ = h
}

func HandlerFunc

func HandlerFunc(name string, f func(http.ResponseWriter, *http.Request)) http.HandlerFunc

HandlerFunc wraps a given http handler func in standard metrics handlers.

Example
package main

import (
	"net/http"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	h := httpmetrics.HandlerFunc("my-handler", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	_ = h
}

func NewIDTokenClient added in v0.5.156

func NewIDTokenClient(ctx context.Context, audience string, opts ...idtoken.ClientOption) (*http.Client, error)

NewIDTokenClient creates a new http.Client based on idtoken.Client, with metrics.

func ScrapeDiskUsage added in v0.5.156

func ScrapeDiskUsage(ctx context.Context)

func ServeMetrics

func ServeMetrics()

ServeMetrics serves the metrics endpoint if the METRICS_PORT env var is set.

func ServerRevisionHandler added in v1.47.7

func ServerRevisionHandler(revision string, handler http.Handler) http.Handler

ServerRevisionHandler adds revision to HTTP responses before calling handler, including responses with error statuses. Pass the service's K_REVISION value at startup; an empty revision leaves responses unchanged. It passes the original ResponseWriter through, preserving support for streaming and hijacking. Place it inside authentication middleware to report only on authenticated requests. The handler is safe for concurrent use if handler is.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	// In Cloud Run, supply the process's K_REVISION value at startup.
	handler := httpmetrics.ServerRevisionHandler("api-00042-abc", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}))
	response := httptest.NewRecorder()
	handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", nil))
	fmt.Println(response.Header().Get(httpmetrics.ServerRevisionHeader))
}
Output:
api-00042-abc

func ServerRevisionStreamInterceptor added in v1.47.7

func ServerRevisionStreamInterceptor(revision string) grpc.StreamServerInterceptor

ServerRevisionStreamInterceptor identifies the serving revision in streaming gRPC response headers, including application errors. Metadata is queued for the stream's normal header send. Pass the service's K_REVISION value at startup. An empty revision emits no metadata. Install it once, after authentication, and reserve ServerRevisionHeader for this interceptor. It is safe for concurrent use. If headers cannot be added (for example, an earlier interceptor already sent them), the application handler still runs unchanged.

Example
package main

import (
	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
	"google.golang.org/grpc"
)

func main() {
	server := grpc.NewServer(grpc.ChainStreamInterceptor(
		// Add authentication before the revision interceptor.
		httpmetrics.ServerRevisionStreamInterceptor("api-00042-abc"),
	))
	defer server.Stop()
}

func ServerRevisionUnaryInterceptor added in v1.47.7

func ServerRevisionUnaryInterceptor(revision string) grpc.UnaryServerInterceptor

ServerRevisionUnaryInterceptor identifies the serving revision in unary gRPC response headers, including application errors. Pass the service's K_REVISION value at startup. An empty revision emits no metadata. Install it once, after authentication, and reserve ServerRevisionHeader for this interceptor. It is safe for concurrent use. If headers cannot be added (for example, an earlier interceptor already sent them), the application handler still runs unchanged.

Example
package main

import (
	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
	"google.golang.org/grpc"
)

func main() {
	server := grpc.NewServer(grpc.ChainUnaryInterceptor(
		// Add authentication before the revision interceptor.
		httpmetrics.ServerRevisionUnaryInterceptor("api-00042-abc"),
	))
	defer server.Stop()
}

func SetBucketSuffixes

func SetBucketSuffixes(bs map[string]string)

SetBucketSuffixes configures suffix-based host-to-label mappings. Must be called before the first HTTP request for the mappings to take effect.

Example
package main

import (
	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	httpmetrics.SetBucketSuffixes(map[string]string{
		"example.com": "example",
	})
}

func SetBuckets

func SetBuckets(b map[string]string)

SetBuckets configures exact host-to-label mappings. Must be called before the first HTTP request for the mappings to take effect.

Example
package main

import (
	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	httpmetrics.SetBuckets(map[string]string{
		"api.example.com": "example-api",
	})
}

func SetupMetrics added in v1.0.1

func SetupMetrics(ctx context.Context) func()

SetupMetrics setups a prometheus exporter for otel metrics, and starts memusage.Heartbeat, which logs the container's memory use while it is at or over half its limit. The heartbeat outlives ctx so that it still logs while the process drains after SIGTERM.

Expected usage:

defer metrics.SetupMetrics(ctx)()
Example
package main

import (
	"context"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	ctx := context.Background()
	cleanup := httpmetrics.SetupMetrics(ctx)
	defer cleanup()
}

func SetupTracer

func SetupTracer(ctx context.Context) func()

Fractions >= 1 will always sample. Fractions < 0 are treated as zero. To respect the parent trace's `SampledFlag`, the `TraceIDRatioBased` sampler should be used as a delegate of a `Parent` sampler.

Expected usage:

defer metrics.SetupTracer(ctx)()
Example
package main

import (
	"context"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	ctx := context.Background()
	cleanup := httpmetrics.SetupTracer(ctx)
	defer cleanup()
}

func SetupTracerWith added in v1.55.1

func SetupTracerWith(ctx context.Context, opts ...TracerOption) func()

SetupTracerWith is SetupTracer configured by opts.

Example
package main

import (
	"context"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	// A process that cannot reach the metadata server skips the probe; an
	// exporter named in OTEL_TRACES_EXPORTER still installs.
	ctx := context.Background()
	cleanup := httpmetrics.SetupTracerWith(ctx, httpmetrics.NoGCPDetection())
	defer cleanup()
}

func WithGitHubAppID added in v1.0.4

func WithGitHubAppID(ctx context.Context, appID int64) context.Context

WithGitHubAppID returns a copy of ctx with the GitHub App ID attached. The transport reads this value to label rate limit metrics with the app that made the request, enabling per-app visibility into quota consumption.

func WithGitHubInstallationID added in v1.0.4

func WithGitHubInstallationID(ctx context.Context, installationID int64) context.Context

WithGitHubInstallationID returns a copy of ctx with the GitHub installation ID attached. The transport reads this value to label rate limit metrics with the installation that made the request. GitHub enforces rate limits per installation, so this label identifies which (app, org) pair is consuming quota.

func WrapTransport

func WrapTransport(t http.RoundTripper, opts ...TransportOption) http.RoundTripper

WrapTransport wraps an http.RoundTripper with instrumentation.

Example
package main

import (
	"net/http"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	t := httpmetrics.WrapTransport(http.DefaultTransport)
	_ = t
}
Example (SkipBucketize)
package main

import (
	"net/http"

	"github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
)

func main() {
	t := httpmetrics.WrapTransport(http.DefaultTransport, httpmetrics.WithSkipBucketize(true))
	_ = t
}

Types

type MetricsTransport added in v0.5.156

type MetricsTransport struct {
	http.RoundTripper
	// contains filtered or unexported fields
}

type TracerOption added in v1.55.1

type TracerOption func(*tracerConfig)

TracerOption configures SetupTracerWith.

func NoGCPDetection added in v1.55.1

func NoGCPDetection() TracerOption

NoGCPDetection skips the metadata-server probe that decides whether the process runs on GCP, and treats it as off GCP: no Cloud Trace exporter unless OTEL_TRACES_EXPORTER names one, and no GCP resource detector. Exporters named in OTEL_TRACES_EXPORTER, OTLP included, still install. It is for a process that cannot reach the metadata server, where the probe would wait out its 2s timeout at every start. OTEL_TRACES_EXPORTER=gcp still needs credentials that name a project, and exits without them, as it does off GCP.

type TransportOption added in v0.6.168

type TransportOption func(*metricsTransportOptions)

func WithSkipBucketize added in v0.6.168

func WithSkipBucketize(skip bool) TransportOption

WithSkipBucketize is a TransportOption that skips the bucketization of the host. This is useful for transports that talk to an unbounded number of hosts, where bucketization would cause excessive metric cardinality. If true, the host label will be set to "unbucketized".

func WithTracePropagation added in v1.30.2

func WithTracePropagation(propagate bool) TransportOption

WithTracePropagation controls whether outbound requests carry the caller's trace context. It defaults to true. When false, the client span is still recorded but no trace context is injected onto the outbound request.

type TransportUnwrapper added in v1.0.3

type TransportUnwrapper interface {
	Unwrap() http.RoundTripper
}

TransportUnwrapper is implemented by RoundTripper wrappers that can expose their underlying transport. This follows the same convention as errors.Unwrap.

Directories

Path Synopsis
Package cloudevents provides helpers for creating CloudEvents HTTP clients and targets that are instrumented with httpmetrics middleware.
Package cloudevents provides helpers for creating CloudEvents HTTP clients and targets that are instrumented with httpmetrics middleware.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL