Custom Metrics in Go
Emit custom metrics with attributes from Go services using the OpenTelemetry SDK and send them to Last9
Use the OpenTelemetry Go SDK to emit custom metrics — counters, histograms, and gauges — with attributes (labels) from any Go service.
Prerequisites
- Go 1.22 or higher
- Last9 account with OTLP credentials
Installation
-
Install dependencies
go get go.opentelemetry.io/otel \go.opentelemetry.io/otel/metric \go.opentelemetry.io/otel/sdk/metric \go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp -
Set environment variables
export OTEL_SERVICE_NAME="your-service"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" -
Set up MeterProvider
package mainimport ("context""log""time""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp""go.opentelemetry.io/otel/sdk/metric""go.opentelemetry.io/otel/sdk/resource"semconv "go.opentelemetry.io/otel/semconv/v1.26.0")func initMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {exporter, err := otlpmetrichttp.New(ctx)if err != nil {return nil, err}res, err := resource.New(ctx,resource.WithFromEnv(),resource.WithAttributes(semconv.ServiceNameKey.String("your-service"),semconv.DeploymentEnvironmentKey.String("production"),),)if err != nil {return nil, err}mp := metric.NewMeterProvider(metric.WithResource(res),metric.WithReader(metric.NewPeriodicReader(exporter,metric.WithInterval(30*time.Second),)),)otel.SetMeterProvider(mp)return mp, nil}func main() {ctx := context.Background()mp, err := initMeterProvider(ctx)if err != nil {log.Fatalf("init meter provider: %v", err)}defer func() {mp.ForceFlush(ctx)mp.Shutdown(ctx)}()// your application code here} -
Record metrics with attributes
import ("go.opentelemetry.io/otel/attribute"otelmetric "go.opentelemetry.io/otel/metric")meter := otel.Meter("your-service")counter, err := meter.Int64Counter("subscription.upgrade.notification.resolution",otelmetric.WithDescription("Count of subscription upgrade notification resolutions"),)if err != nil {log.Fatalf("create counter: %v", err)}// Record with attributescounter.Add(ctx, 1, otelmetric.WithAttributes(attribute.String("status", status),attribute.String("reason", reason),))meter := otel.Meter("your-service")histogram, err := meter.Float64Histogram("request.duration",otelmetric.WithDescription("Request duration in seconds"),otelmetric.WithUnit("s"),)if err != nil {log.Fatalf("create histogram: %v", err)}histogram.Record(ctx, duration.Seconds(), otelmetric.WithAttributes(attribute.String("method", "GET"),attribute.String("status_code", "200"),))meter := otel.Meter("your-service")gauge, err := meter.Int64Gauge("queue.depth",otelmetric.WithDescription("Current queue depth"),)if err != nil {log.Fatalf("create gauge: %v", err)}gauge.Record(ctx, int64(queueDepth), otelmetric.WithAttributes(attribute.String("queue_name", queueName),))
Metric naming
The OTel SDK converts . to _ in metric names when exporting. A counter named subscription.upgrade.notification.resolution appears in Last9 as subscription_upgrade_notification_resolution_total.
Querying in Last9
Once metrics are flowing, query them in Last9 using PromQL:
# Total rate of resolutions by statussum by (status, reason) ( rate(subscription_upgrade_notification_resolution_total[5m]))
# Filter to a specific attribute valuesubscription_upgrade_notification_resolution_total{status="success"}Troubleshooting
-
Attributes not appearing in Last9
If a metric appears in Last9 but some attributes (labels) are missing, the most likely cause is that the attribute value is an empty string at record time.
Last9 follows the Prometheus data model: labels with empty values are treated as if the label does not exist. The metric is stored and queryable, but that label dimension is absent.
How to identify the problem — add a log line immediately before the
Add()orRecord()call:log.Printf("recording metric: status=%q reason=%q product_type=%q",status, reason, productType)counter.Add(ctx, 1, otelmetric.WithAttributes(attribute.String("status", status),attribute.String("reason", reason),attribute.String("product_type", productType),))If any value prints as
"", that label will not appear in Last9.Fix — use a sentinel value instead of empty string:
if status == "" {status = "unknown"}if reason == "" {reason = "unknown"}if productType == "" {productType = "unknown"}counter.Add(ctx, 1, otelmetric.WithAttributes(attribute.String("status", status),attribute.String("reason", reason),attribute.String("product_type", productType),))This behavior is silent — no error is returned and the metric value is recorded correctly. Only the label is dropped. This makes it easy to miss during development if attribute values happen to be non-empty in test environments but empty in production.
Please get in touch with us on Discord or Email if you have any questions.