Skip to content

Conversation

@acofer
Copy link
Contributor

@acofer acofer commented Dec 9, 2025

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds OpenTelemetry instrumentation to the codebase as an alternative/complementary telemetry solution alongside the existing DataDog StatsD integration. The implementation introduces support for OpenTelemetry traces, metrics, and logs, enabling observability through OTLP exporters and optional Prometheus metrics.

Key Changes

  • Added comprehensive OpenTelemetry SDK setup with support for traces, metrics, and logs via OTLP exporters
  • Refactored shared logging structures (logMsg, iso8601) from statsd.go to middlewares.go for reuse across telemetry implementations
  • Extended metrics monitoring to emit both DataDog and OpenTelemetry metrics simultaneously

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
middlewares/otel.go New file implementing OpenTelemetry setup, log writer, and event logger integration with zap
middlewares/statsd.go Removed logMsg struct and related code, now consolidated in middlewares.go
middlewares/middlewares.go Added shared logMsg struct and iso8601 constant moved from statsd.go, plus encoding/json import
middlewares/metrics.go Extended monitor() function to emit OpenTelemetry metrics alongside DataDog metrics
go.mod Added OpenTelemetry dependencies and updated Go version/toolchain specifications
go.sum Updated checksums for new and updated dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if err != nil {
return
}
ctx := context.Background()
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Using context.Background() here loses any context propagation from the HTTP request. Consider passing the request context (e.g., from r.Context()) through to this function to maintain context propagation for tracing and cancellation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have the context for now.

Comment on lines 153 to 196
func (o OpenTelemetryWriter) Write(p []byte) (n int, err error) {
var msg logMsg
if err = json.Unmarshal(p, &msg); err != nil {
return
}
n = len(p)

var timestamp time.Time
timestamp, err = time.Parse(iso8601, msg.Timestamp)
if err != nil {
return
}

tags := msg.Tags
if tags == nil {
tags = []string{}
}
tagValues := make([]log.Value, len(tags))
for i, v := range tags {
tagValues[i] = log.StringValue(v)
}
tagValue := log.SliceValue(tagValues...)

severity := log.SeverityDebug
severityText := "debug"
if sm, ok := severityMap[msg.Level]; ok {
severity = sm
severityText = msg.Level
}

record := log.Record{}
record.SetTimestamp(timestamp)
record.SetEventName(msg.Name)
record.SetSeverity(severity)
record.SetSeverityText(severityText)
record.AddAttributes(log.KeyValue{
Key: "tags",
Value: tagValue,
})
record.SetBody(log.StringValue(msg.Text()))

o.logger.Emit(o.ctx, record)
return
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The error handling in the Write method is incorrect. When an error occurs (lines 155-156 or 161-163), the function returns with n = 0 initially, but if the error occurs after line 158, n is already set to len(p). This violates the io.Writer contract which states that if an error is returned, the number of bytes written should accurately reflect what was successfully written (typically 0 for a failed write). The n value should only be set at the end if the write is successful.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not a big problem but this is true, We should follow this pattern

Comment on lines 44 to 55
oCount, err := oMeter.Int64Counter("http_request_count")
if err != nil {
return
}
oDuration, err := oMeter.Float64Histogram("http_request_duration")
if err != nil {
return
}
oStatusCount, err := oMeter.Int64Counter(fmt.Sprintf("http_request_status_%s", statusType(httpCode)))
if err != nil {
return
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Creating new metric instruments (Counter/Histogram) on every request is inefficient. These instruments should be created once at initialization time and reused. Consider creating these instruments in an init function or during setup, and storing them as package-level variables or in a struct.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for your work on this! I noticed that in the official OpenTelemetry example, variables like oCount, oDuration, and oStatusCount are created once at startup, since they are not global. While the end result is the same, creating these instruments on every request could be inefficient.

I think We could store the instruments as package-level variables, and for dynamic counters like http_request_status_%s, use a map to create them only if they don’t already exist. What do you think about this approach? Let me know if there’s a specific reason for the current implementation!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I was thinking about this last night, and we probably need a global map of counters etc. Will get this done this afternoon.

Comment on lines 44 to 55
oCount, err := oMeter.Int64Counter("http_request_count")
if err != nil {
return
}
oDuration, err := oMeter.Float64Histogram("http_request_duration")
if err != nil {
return
}
oStatusCount, err := oMeter.Int64Counter(fmt.Sprintf("http_request_status_%s", statusType(httpCode)))
if err != nil {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for your work on this! I noticed that in the official OpenTelemetry example, variables like oCount, oDuration, and oStatusCount are created once at startup, since they are not global. While the end result is the same, creating these instruments on every request could be inefficient.

I think We could store the instruments as package-level variables, and for dynamic counters like http_request_status_%s, use a map to create them only if they don’t already exist. What do you think about this approach? Let me know if there’s a specific reason for the current implementation!

if err != nil {
return
}
ctx := context.Background()
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have the context for now.

Comment on lines 153 to 196
func (o OpenTelemetryWriter) Write(p []byte) (n int, err error) {
var msg logMsg
if err = json.Unmarshal(p, &msg); err != nil {
return
}
n = len(p)

var timestamp time.Time
timestamp, err = time.Parse(iso8601, msg.Timestamp)
if err != nil {
return
}

tags := msg.Tags
if tags == nil {
tags = []string{}
}
tagValues := make([]log.Value, len(tags))
for i, v := range tags {
tagValues[i] = log.StringValue(v)
}
tagValue := log.SliceValue(tagValues...)

severity := log.SeverityDebug
severityText := "debug"
if sm, ok := severityMap[msg.Level]; ok {
severity = sm
severityText = msg.Level
}

record := log.Record{}
record.SetTimestamp(timestamp)
record.SetEventName(msg.Name)
record.SetSeverity(severity)
record.SetSeverityText(severityText)
record.AddAttributes(log.KeyValue{
Key: "tags",
Value: tagValue,
})
record.SetBody(log.StringValue(msg.Text()))

o.logger.Emit(o.ctx, record)
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not a big problem but this is true, We should follow this pattern

go.mod Outdated
go 1.24
go 1.24.0

toolchain go1.24.4
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not common on our projects to have this, I usually avoid to do this, is this expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I'm pretty sure GoLand "helpfully" added this for me. I'll revert it.

Copy link
Contributor Author

@acofer acofer Dec 11, 2025

Choose a reason for hiding this comment

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

This is apparently a new thing as of go 1.21 - just running go build or go mod tidy from the command line insists on adding it. 😖 I think we have to live with it. cli/cli#9489

@gzapatas gzapatas self-requested a review December 12, 2025 14:11
Copy link
Contributor

@gzapatas gzapatas left a comment

Choose a reason for hiding this comment

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

LGTM!

@acofer acofer merged commit 5ea5089 into master Dec 12, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants