Skip to content
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
34 changes: 20 additions & 14 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,19 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
err = errors.Join(inErr, shutdown(ctx))
}

res, _ := resource.New(ctx,
resource.WithFromEnv(), // reads OTEL_SERVICE_NAME & OTEL_RESOURCE_ATTRIBUTES.
resource.WithProcess(),
resource.WithHost(),
resource.WithTelemetrySDK(),
)

// 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 +64,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 +73,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,43 +91,41 @@ 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) {
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) {
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
}
139 changes: 118 additions & 21 deletions examples/dice/instrumented/rolldice.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
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"
Expand All @@ -19,42 +22,136 @@
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 last rolls value observed"),
)
if err != nil {
panic(err)
}

// Register the gauge callback.
_, err = meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {

Check failure on line 62 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
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) {
const maxRolls = 1000 // Arbitrary limit to prevent Slice memory allocation with excessive size value.

// 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"
}

// 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)
Copy link
Member

Choose a reason for hiding this comment

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

what about Content-Type?

Copy link
Author

Choose a reason for hiding this comment

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

Thought of leaving it to the defaults as it's a simple app and not explicitly specified in the reference. But I will set application/json as that's how the app responds.

msg := "Parameter rolls must be a positive integer"
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "error",
"message": msg,
})
logger.WarnContext(r.Context(), msg)
return
}

results, err := rollDice(r.Context(), rolls)
if err != nil || rolls > maxRolls {
// Signals invalid input (<=0).
w.WriteHeader(http.StatusInternalServerError)
logger.ErrorContext(r.Context(), err.Error())
Copy link
Member

@flc1125 flc1125 Nov 5, 2025

Choose a reason for hiding this comment

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

Since the condition if err != nil || rolls > maxRolls { implies that err might be nil.

Of course, once maxRolls is removed, this issue is naturally resolved.

return
}

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.
if player == "" {
logger.DebugContext(r.Context(), "anonymous player rolled", "results", results)
} else {
logger.DebugContext(r.Context(), "player", player, "results", results)

Check failure on line 110 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

slog: slog.Logger.DebugContext arg "results" should be a string or a slog.Attr (possible missing key or value) (govet)
}
logger.InfoContext(r.Context(), "Some player rolled a dice.")

var msg string
if player := r.PathValue("player"); player != "" {
msg = player + " is rolling the dice"
w.Header().Set("Content-Type", "application/json")
if len(results) == 1 {
json.NewEncoder(w).Encode(results[0])

Check failure on line 116 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
} else {
msg = "Anonymous player is rolling the dice"
json.NewEncoder(w).Encode(results)

Check failure on line 118 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
}
}

// rollDice is the outer function which Does the error handling.
func rollDice(ctx context.Context, rolls int) ([]int, error) {
ctx, span := tracer.Start(ctx, "rollDice")
defer span.End()

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

results := make([]int, rolls)

Check failure

Code scanning / CodeQL

Slice memory allocation with excessive size value High

This memory allocation depends on a
user-provided value
.
for i := 0; i < rolls; i++ {

Check failure on line 134 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

for loop can be changed to use an integer range (Go 1.22+) (intrange)
results[i] = rollOnce(ctx)
outcomeHist.Record(ctx, int64(results[i]))
}
logger.InfoContext(ctx, msg, "result", roll)

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

// rollOnce is the inner function — returns a random number 1–6.
func rollOnce(ctx context.Context) int {
ctx, span := tracer.Start(ctx, "rollOnce")

Check failure on line 148 in examples/dice/instrumented/rolldice.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to ctx (ineffassign)
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.

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
Loading