Discord Twitter GitHub

Strongly Typed Metric Labels Using Generics in Go

Michael Whittaker
August 1, 2023

Service Weaver is a programming framework for writing distributed systems. An essential part of building distributed systems is monitoring a system's behavior with metrics. For a system that serves HTTP traffic, for example, you likely want to export an http_requests metric that counts the number of HTTP requests your system receives.

Metrics are typically labeled, allowing you to dissect metric measurements across various dimensions. For example, you may want to add a path label to the http_requests metrics that tracks the path against which HTTP requests are issued (e.g., "/", "/users/ava", "/settings/profile") and a status_code label that tracks the HTTP response status code (e.g., 404, 500).

Existing APIs

With most metric libraries, you specify metric labels as a set of key-value pairs. For example, here is how to declare an http_requests counter with path and status_code labels using Prometheus' Go API:

requests := prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests",
        Help: "Number of HTTP requests.",
    },
    []string{"path", "status_code"},
)

And here is how to increment the counter:

requests.With(prometheus.Labels{"path": "/foo", "status_code": "404"}).Inc()

Similarly, here is how to declare the same metric using OpenTelemetry's Go API:

requests, err := meter.Int64Counter(
    "http_requests",
    metric.WithDescription("Number of HTTP requests."),
)

Note that there isn't a way for us to specify the path and status_code labels when we declare the counter. Instead, we provide the labels (what OpenTelemetry calls attributes) when we increment the counter:

requests.Add(ctx, 1, metric.WithAttributes(
    attribute.Key("path").String("/foo"),
    attribute.Key("status_code").Int(404),
))

Drawbacks

Specifying labels as a set of key-value pairs like this has a number of disadvantages.

Service Weaver's Approach

Service Weaver avoids these drawbacks by integrating metric labels with Go's type system. Specifically, Service Weaver represents metric labels as structs and uses generics to instantiate metrics. Here's how to declare the http_requests metric using Service Weaver's API:

type labels struct {
    Path       string `weaver:"path"`
    StatusCode int    `weaver:"status_code"`
}

var requests = metrics.NewCounterMap[labels](
    "http_requests",
    "Number of HTTP requests.",
)

And here's how to increment the counter:

requests.Get(labels{Path: "/foo", StatusCode: 404}).Inc()

Labels can be any struct where every field is a string, bool, or integer. NewCounterMap panics if you call it with an invalid label struct. By leveraging Go's type system, Service Weaver's API avoids the disadvantages of key-value labels described above.

These strongly typed metric labels are just one of the ways that Service Weaver makes it easier to write distributed systems. Read the Service Weaver documentation to learn about more about metrics (including counters, gauges, and histograms) as well as other useful features.