Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f89e2b5
Updating uninstrumented example
lightsta1ker Oct 30, 2025
a337ba6
Updating instrumented example
lightsta1ker Oct 30, 2025
da2e8cd
chore: lint and suggestions
lightsta1ker Nov 4, 2025
08b7f61
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 4, 2025
6697966
chore: rebase & refactor suggestions
lightsta1ker Nov 4, 2025
ddf3c68
chore: refactor suggestions
lightsta1ker Nov 4, 2025
ccea55d
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 4, 2025
fbacc37
fix: Limiting memory allocation for rolls
lightsta1ker Nov 4, 2025
a474139
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 5, 2025
6d00f40
chore: consolidating rollDice observability logic
lightsta1ker Nov 5, 2025
c066c03
chore: refactoring suggestion
lightsta1ker Nov 5, 2025
ae85955
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 19, 2025
96f2473
fix: lint fixes & error handling
lightsta1ker Nov 19, 2025
05421c3
fix: refactoring suggestions
lightsta1ker Nov 19, 2025
2edd445
chore: format with gofumpt
lightsta1ker Nov 19, 2025
06b7cc6
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 19, 2025
4440f23
Merge branch 'main' into dice/issue-7937
lightsta1ker Nov 24, 2025
837eca3
chore: rebasing & error handling changes
lightsta1ker Nov 24, 2025
20455e4
Merge branch 'main' into dice/issue-7937
lightsta1ker Dec 3, 2025
e058c34
Merge branch 'main' into dice/issue-7937
lightsta1ker Dec 3, 2025
3659825
Merge branch 'main' into dice/issue-7937
pellared Dec 17, 2025
8501bc4
Update examples/dice/instrumented/rolldice.go
lightsta1ker Jan 9, 2026
f3baabb
Merge branch 'main' into dice/issue-7937
pellared Jan 9, 2026
35740f7
Update examples/dice/instrumented/otel.go
lightsta1ker Jan 28, 2026
36394bb
chore: using otel-codes
lightsta1ker Jan 28, 2026
76387e9
chore: resolving conflicts
lightsta1ker Jan 28, 2026
7179932
fix: merge conflict
lightsta1ker Jan 31, 2026
da08360
chore: golangci-lint fix
lightsta1ker Jan 31, 2026
2a0b02f
chore: resolving conflicts
lightsta1ker May 1, 2026
49f04e2
chore: lint issue fix
lightsta1ker May 1, 2026
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
5 changes: 0 additions & 5 deletions examples/dice/instrumented/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,3 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/sys v0.43.0 // indirect
)

replace (
go.opentelemetry.io/contrib/bridges/otelslog => ../../../bridges/otelslog
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => ../../../instrumentation/net/http/otelhttp
)
4 changes: 4 additions & 0 deletions examples/dice/instrumented/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM=
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
Expand Down
9 changes: 6 additions & 3 deletions examples/dice/instrumented/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ func run() error {
}()

// Start HTTP server.
port := os.Getenv("APPLICATION_PORT")
if port == "" {
port = "8080"
}
srv := &http.Server{
Addr: ":8080",
Addr: ":" + port,
BaseContext: func(net.Listener) context.Context { return ctx },
ReadTimeout: time.Second,
WriteTimeout: 10 * time.Second,
Expand Down Expand Up @@ -72,8 +76,7 @@ func newHTTPHandler() http.Handler {
mux := http.NewServeMux()

// Register handlers.
mux.Handle("/rolldice", http.HandlerFunc(rolldice))
mux.Handle("/rolldice/{player}", http.HandlerFunc(rolldice))
mux.Handle("/rolldice", http.HandlerFunc(handleRolldice))

// Add HTTP instrumentation for the whole server.
handler := otelhttp.NewHandler(mux, "/")
Expand Down
42 changes: 27 additions & 15 deletions examples/dice/instrumented/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
)

Expand All @@ -42,12 +43,23 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
err = errors.Join(inErr, shutdown(ctx))
}

res, resErr := resource.New(ctx,
resource.WithFromEnv(), // Parses OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
resource.WithProcess(),
resource.WithHost(),
resource.WithTelemetrySDK(),
)
if resErr != nil {
handleErr(resErr)
return shutdown, resErr
}

// Set up propagator.
prop := newPropagator()
otel.SetTextMapPropagator(prop)

// Set up trace provider.
tracerProvider, err := newTracerProvider()
tracerProvider, err := newTracerProvider(res)
if err != nil {
handleErr(err)
return shutdown, err
Expand All @@ -56,7 +68,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
otel.SetTracerProvider(tracerProvider)

// Set up meter provider.
meterProvider, err := newMeterProvider()
meterProvider, err := newMeterProvider(res)
if err != nil {
handleErr(err)
return shutdown, err
Expand All @@ -65,7 +77,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
otel.SetMeterProvider(meterProvider)

// Set up logger provider.
loggerProvider, err := newLoggerProvider()
loggerProvider, err := newLoggerProvider(res)
if err != nil {
handleErr(err)
return shutdown, err
Expand All @@ -83,42 +95,42 @@ func newPropagator() propagation.TextMapPropagator {
)
}

func newTracerProvider() (*trace.TracerProvider, error) {
traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
func newTracerProvider(res *resource.Resource) (*trace.TracerProvider, error) {
traceExporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint())
if err != nil {
return nil, err
}

tracerProvider := trace.NewTracerProvider(
trace.WithBatcher(traceExporter,
// Default is 5s. Set to 1s for demonstrative purposes.
trace.WithBatchTimeout(time.Second)),
trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second)), // Default is 5s. Set to 1s for demonstrative purposes.
trace.WithResource(res),
)
return tracerProvider, nil
}

func newMeterProvider() (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New(stdoutmetric.WithPrettyPrint())
func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}

meterProvider := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// Default is 1m. Set to 3s for demonstrative purposes.
metric.WithInterval(3*time.Second))),
metric.WithReader(metric.NewPeriodicReader(metricExporter, metric.WithInterval(3*time.Second))), // Default is 1m. Set to 3s for demonstrative purposes.
metric.WithResource(res),
)
return meterProvider, nil
}

func newLoggerProvider() (*log.LoggerProvider, error) {
logExporter, err := stdoutlog.New(stdoutlog.WithPrettyPrint())
func newLoggerProvider(res *resource.Resource) (*log.LoggerProvider, error) {
logExporter, err := stdoutlog.New()
if err != nil {
return nil, err
}

loggerProvider := log.NewLoggerProvider(
log.WithProcessor(log.NewBatchProcessor(logExporter)),
log.WithResource(res),
)
return loggerProvider, nil
}
167 changes: 146 additions & 21 deletions examples/dice/instrumented/rolldice.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
package main

import (
"io"
"math/rand"
"context"
"encoding/json"
"errors"
"math/rand/v2"
"net/http"
"strconv"
"sync/atomic"

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

"go.opentelemetry.io/contrib/bridges/otelslog"
Expand All @@ -19,42 +23,163 @@ import (
const name = "go.opentelemetry.io/contrib/examples/dice"

var (
tracer = otel.Tracer(name)
meter = otel.Meter(name)
logger = otelslog.NewLogger(name)
rollCnt metric.Int64Counter
tracer = otel.Tracer(name)
meter = otel.Meter(name)
logger = otelslog.NewLogger(name)
rollCnt metric.Int64Counter
outcomeHist metric.Int64Histogram
lastRollsGauge metric.Int64ObservableGauge
lastRolls atomic.Int64
)

func init() {
var err error
rollCnt, err = meter.Int64Counter("dice.rolls",
metric.WithDescription("The number of rolls by roll value"),
metric.WithDescription("The number of rolls"),
metric.WithUnit("{roll}"))
if err != nil {
panic(err)
}

outcomeHist, err = meter.Int64Histogram(
"dice.outcome",
metric.WithDescription("Distribution of dice outcomes (1-6)"),
metric.WithUnit("{count}"),
)
if err != nil {
panic(err)
}

lastRollsGauge, err = meter.Int64ObservableGauge(
"dice.last.rolls",
metric.WithDescription("The most recent rolled value"),
)
if err != nil {
panic(err)
}

// Register the gauge callback.
_, err = meter.RegisterCallback(
func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(lastRollsGauge, lastRolls.Load())
return nil
},
lastRollsGauge,
)
if err != nil {
panic(err)
}
}

func rolldice(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "roll")
defer span.End()
func handleRolldice(w http.ResponseWriter, r *http.Request) {
// Parse query parameters.
rollsParam := r.URL.Query().Get("rolls")
player := r.URL.Query().Get("player")

// Default rolls = 1 if not defined.
if rollsParam == "" {
rollsParam = "1"
}

roll := 1 + rand.Intn(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive.
// Check if rolls is a number.
rolls, err := strconv.Atoi(rollsParam)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
Comment thread
pellared marked this conversation as resolved.
msg := "Parameter rolls must be a positive integer"
_ = json.NewEncoder(w).Encode(map[string]string{
Comment thread
pellared marked this conversation as resolved.
"status": "error",
"message": msg,
})
logger.WarnContext(r.Context(), msg)
return
}

var msg string
if player := r.PathValue("player"); player != "" {
msg = player + " is rolling the dice"
results, err := rollDice(r.Context(), rolls)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
msg := "Internal server error"
Comment thread
pellared marked this conversation as resolved.
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "error",
"message": msg,
})
logger.ErrorContext(r.Context(), err.Error())
Comment thread
pellared marked this conversation as resolved.
return
}

if player == "" {
logger.DebugContext(r.Context(), "anonymous player rolled", "results", results)
} else {
msg = "Anonymous player is rolling the dice"
logger.DebugContext(r.Context(), "player rolled dice", "player", player, "results", results)
}
Comment thread
pellared marked this conversation as resolved.
logger.InfoContext(ctx, msg, "result", roll)
logger.InfoContext(r.Context(), "Some player rolled a dice.")
Comment thread
pellared marked this conversation as resolved.
Comment thread
pellared marked this conversation as resolved.
Comment thread
pellared marked this conversation as resolved.

w.Header().Set("Content-Type", "application/json")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Content-Type header is set twice for successful responses - once at line 117 before calling writeJSON, and again inside writeJSON at line 72. The duplicate header setting at line 117 should be removed since writeJSON already sets it.

Suggested change
w.Header().Set("Content-Type", "application/json")

Copilot uses AI. Check for mistakes.
if len(results) == 1 {
writeJSON(r.Context(), w, results[0])
} else {
writeJSON(r.Context(), w, results)
}
}

func writeJSON(ctx context.Context, w http.ResponseWriter, v any) {
data, err := json.Marshal(v)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
Comment thread
pellared marked this conversation as resolved.
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "error",
"message": "Internal Server Error",
})
logger.ErrorContext(ctx, "json encode failed", "error", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}

func rollDice(ctx context.Context, rolls int) ([]int, error) {
const maxRolls = 1000 // Arbitrary limit to prevent Slice memory allocation with excessive size value.

ctx, span := tracer.Start(ctx, "rollDice")
defer span.End()

if rolls > maxRolls {
err := errors.New("rolls parameter exceeds maximum allowed value")
span.SetStatus(codes.Error, err.Error())
return nil, err
}

if rolls <= 0 {
err := errors.New("rolls must be positive")
span.SetStatus(codes.Error, err.Error())
return nil, err
}

results := make([]int, rolls)
Comment thread Fixed
for i := range rolls {
Comment on lines +161 to +162
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uninstrumented version has special handling when rolls==1 (lines 88-89) that returns early without allocating a slice and using a loop. However, the instrumented version doesn't have this optimization and always allocates a slice and uses a loop even for a single roll. This creates an inconsistency in behavior between the two versions.

Suggested change
results := make([]int, rolls)
for i := range rolls {
if rolls == 1 {
// Fast path for a single roll to mirror the uninstrumented implementation.
result := rollOnce(ctx)
outcomeHist.Record(ctx, int64(result))
rollsAttr := attribute.Int("rolls", rolls)
span.SetAttributes(rollsAttr)
rollCnt.Add(ctx, int64(rolls), metric.WithAttributes(rollsAttr))
lastRolls.Store(int64(rolls))
return []int{result}, nil
}
results := make([]int, rolls)
for i := range results {

Copilot uses AI. Check for mistakes.
results[i] = rollOnce(ctx)
outcomeHist.Record(ctx, int64(results[i]))
}

rollsAttr := attribute.Int("rolls", rolls)
span.SetAttributes(rollsAttr)
rollCnt.Add(ctx, int64(rolls), metric.WithAttributes(rollsAttr))
lastRolls.Store(int64(rolls))
return results, nil
}

// rollOnce returns a random number between 1 and 6.
func rollOnce(ctx context.Context) int {
_, span := tracer.Start(ctx, "rollOnce")
defer span.End()

roll := 1 + rand.IntN(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive.
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instrumented version uses math/rand/v2 and rand.IntN, while the uninstrumented version uses math/rand and rand.Intn. For consistency and to serve as a proper reference example, both versions should use the same random number generation approach.

Copilot uses AI. Check for mistakes.

rollValueAttr := attribute.Int("roll.value", roll)
span.SetAttributes(rollValueAttr)
rollCnt.Add(ctx, 1, metric.WithAttributes(rollValueAttr))

resp := strconv.Itoa(roll) + "\n"
if _, err := io.WriteString(w, resp); err != nil {
logger.ErrorContext(ctx, "Write failed", "error", err)
}
return roll
}
11 changes: 7 additions & 4 deletions examples/dice/uninstrumented/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ func run() (err error) {
defer stop()

// Start HTTP server.
port := os.Getenv("APPLICATION_PORT")
if port == "" {
port = "8080"
}

srv := &http.Server{
Addr: ":8080",
Addr: ":" + port,
BaseContext: func(net.Listener) context.Context { return ctx },
ReadTimeout: time.Second,
WriteTimeout: 10 * time.Second,
Expand Down Expand Up @@ -60,8 +65,6 @@ func newHTTPHandler() http.Handler {
mux := http.NewServeMux()

// Register handlers.
mux.HandleFunc("/rolldice/", rolldice)
mux.HandleFunc("/rolldice/{player}", rolldice)

mux.HandleFunc("/rolldice", handleRolldice)
return mux
}
Loading