Skip to content

Commit 6b58e9c

Browse files
feat(go): add custom metrics example with OTel SDK
Demonstrates counter, histogram, and gauge using raw OTel Go SDK with OTLP HTTP export. Includes guard against empty string attribute values which are silently dropped per the Prometheus data model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 637d501 commit 6b58e9c

4 files changed

Lines changed: 230 additions & 0 deletions

File tree

go/custom-metrics/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
custom-metrics-example
2+
example

go/custom-metrics/go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module custom-metrics-example
2+
3+
go 1.22.7
4+
5+
toolchain go1.22.12
6+
7+
require (
8+
go.opentelemetry.io/otel v1.33.0
9+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0
10+
go.opentelemetry.io/otel/metric v1.33.0
11+
go.opentelemetry.io/otel/sdk v1.33.0
12+
go.opentelemetry.io/otel/sdk/metric v1.33.0
13+
)
14+
15+
require (
16+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
17+
github.com/go-logr/logr v1.4.2 // indirect
18+
github.com/go-logr/stdr v1.2.2 // indirect
19+
github.com/google/uuid v1.6.0 // indirect
20+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
21+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
22+
go.opentelemetry.io/otel/trace v1.33.0 // indirect
23+
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
24+
golang.org/x/net v0.32.0 // indirect
25+
golang.org/x/sys v0.28.0 // indirect
26+
golang.org/x/text v0.21.0 // indirect
27+
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
28+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
29+
google.golang.org/grpc v1.68.1 // indirect
30+
google.golang.org/protobuf v1.35.2 // indirect
31+
)

go/custom-metrics/go.sum

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
2+
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
6+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
7+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
8+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
9+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
10+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
11+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
12+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
13+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
15+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
16+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
17+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
18+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
21+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
22+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
23+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
24+
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
25+
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
26+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8=
27+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU=
28+
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
29+
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
30+
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
31+
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
32+
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
33+
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
34+
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
35+
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
36+
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
37+
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
38+
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
39+
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
40+
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
41+
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
42+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
43+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
44+
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
45+
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
46+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
47+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
48+
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
49+
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
50+
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
51+
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
52+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
53+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

go/custom-metrics/main.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"math/rand"
7+
"time"
8+
9+
"go.opentelemetry.io/otel"
10+
"go.opentelemetry.io/otel/attribute"
11+
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
12+
otelmetric "go.opentelemetry.io/otel/metric"
13+
"go.opentelemetry.io/otel/sdk/metric"
14+
"go.opentelemetry.io/otel/sdk/resource"
15+
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
16+
)
17+
18+
func initMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
19+
exporter, err := otlpmetrichttp.New(ctx)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
res, err := resource.New(ctx,
25+
resource.WithFromEnv(),
26+
resource.WithAttributes(
27+
semconv.ServiceNameKey.String("custom-metrics-example"),
28+
semconv.DeploymentEnvironmentKey.String("development"),
29+
),
30+
)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
mp := metric.NewMeterProvider(
36+
metric.WithResource(res),
37+
metric.WithReader(metric.NewPeriodicReader(exporter,
38+
metric.WithInterval(15*time.Second),
39+
)),
40+
)
41+
otel.SetMeterProvider(mp)
42+
return mp, nil
43+
}
44+
45+
func main() {
46+
ctx := context.Background()
47+
48+
mp, err := initMeterProvider(ctx)
49+
if err != nil {
50+
log.Fatalf("init meter provider: %v", err)
51+
}
52+
defer func() {
53+
if err := mp.ForceFlush(ctx); err != nil {
54+
log.Printf("flush error: %v", err)
55+
}
56+
if err := mp.Shutdown(ctx); err != nil {
57+
log.Printf("shutdown error: %v", err)
58+
}
59+
}()
60+
61+
meter := otel.Meter("custom-metrics-example")
62+
63+
// Counter: tracks how many times an event occurred
64+
resolutionCounter, err := meter.Int64Counter(
65+
"subscription.upgrade.notification.resolution",
66+
otelmetric.WithDescription("Count of subscription upgrade notification resolutions"),
67+
)
68+
if err != nil {
69+
log.Fatalf("create counter: %v", err)
70+
}
71+
72+
// Histogram: tracks distribution of a value (e.g. latency)
73+
requestDuration, err := meter.Float64Histogram(
74+
"request.duration",
75+
otelmetric.WithDescription("Request duration in seconds"),
76+
otelmetric.WithUnit("s"),
77+
)
78+
if err != nil {
79+
log.Fatalf("create histogram: %v", err)
80+
}
81+
82+
// Gauge: tracks a current value that can go up or down
83+
queueDepth, err := meter.Int64Gauge(
84+
"queue.depth",
85+
otelmetric.WithDescription("Current number of items in the queue"),
86+
)
87+
if err != nil {
88+
log.Fatalf("create gauge: %v", err)
89+
}
90+
91+
statuses := []string{"success", "failure", "timeout"}
92+
reasons := []string{"completed", "rejected", "expired"}
93+
products := []string{"premium", "basic", "trial"}
94+
95+
log.Println("emitting metrics every 5s, press Ctrl+C to stop")
96+
97+
for i := 0; i < 10; i++ {
98+
status := statuses[rand.Intn(len(statuses))]
99+
reason := reasons[rand.Intn(len(reasons))]
100+
product := products[rand.Intn(len(products))]
101+
102+
// IMPORTANT: never pass empty string as attribute value.
103+
// Last9 follows the Prometheus data model: labels with empty values
104+
// silently — the metric is recorded but that label dimension is absent.
105+
// Use a sentinel like "unknown" if the value may be empty.
106+
if status == "" {
107+
status = "unknown"
108+
}
109+
if reason == "" {
110+
reason = "unknown"
111+
}
112+
if product == "" {
113+
product = "unknown"
114+
}
115+
116+
resolutionCounter.Add(ctx, 1, otelmetric.WithAttributes(
117+
attribute.String("status", status),
118+
attribute.String("reason", reason),
119+
attribute.String("product_type", product),
120+
))
121+
122+
duration := 0.1 + rand.Float64()*0.9
123+
requestDuration.Record(ctx, duration, otelmetric.WithAttributes(
124+
attribute.String("method", "POST"),
125+
attribute.String("status_code", "200"),
126+
attribute.String("product_type", product),
127+
))
128+
129+
depth := int64(rand.Intn(100))
130+
queueDepth.Record(ctx, depth, otelmetric.WithAttributes(
131+
attribute.String("queue_name", "notifications"),
132+
))
133+
134+
log.Printf("recorded: status=%s reason=%s product_type=%s duration=%.3fs queue_depth=%d",
135+
status, reason, product, duration, depth)
136+
137+
time.Sleep(5 * time.Second)
138+
}
139+
140+
log.Println("done — metrics will appear in Last9 as:")
141+
log.Println(" subscription_upgrade_notification_resolution_total")
142+
log.Println(" request_duration_seconds_bucket / _count / _sum")
143+
log.Println(" queue_depth")
144+
}

0 commit comments

Comments
 (0)