Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 6 additions & 5 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import "github.com/go-coldbrew/core/config"


<a name="Config"></a>
## type [Config](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L6-L137>)
## type [Config](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L6-L138>)

Config is the configuration for the Coldbrew server It is populated from environment variables and has sensible defaults for all fields so that you can just use it as is without any configuration The following environment variables are supported and can be used to override the defaults for the fields

Expand Down Expand Up @@ -195,14 +195,15 @@ type Config struct {
// OTLPSamplingRatio is the ratio of traces to sample (0.0 to 1.0)
// 1.0 means sample all traces, 0.1 means sample 10% of traces
OTLPSamplingRatio float64 `envconfig:"OTLP_SAMPLING_RATIO" default:"0.2"`
// OTLPUseOpenTracingBridge determines whether to set up OpenTracing compatibility bridge
// This allows using existing OpenTracing instrumentation with OpenTelemetry
OTLPUseOpenTracingBridge bool `envconfig:"OTLP_USE_OPENTRACING_BRIDGE" default:"true"`
// Deprecated: OpenTracing bridge is provided for backwards compatibility only.
// New services should leave this false (the default). Set to true only if you
// have existing OpenTracing instrumentation that hasn't been migrated to OTEL.
OTLPUseOpenTracingBridge bool `envconfig:"OTLP_USE_OPENTRACING_BRIDGE" default:"false"`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
```

<a name="Config.Validate"></a>
### func \(Config\) [Validate](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L142>)
### func \(Config\) [Validate](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L143>)

```go
func (c Config) Validate() []string
Expand Down
112 changes: 96 additions & 16 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -24,8 +25,10 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
oteltrace "go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand Down Expand Up @@ -169,37 +172,113 @@ func (c *cb) processConfig() {
}
}

// statusRecorder wraps http.ResponseWriter to capture the final HTTP status code.
// It records the first status >= 200, plus 101 Switching Protocols (which is
// terminal). Other 1xx statuses are informational and skipped.
// Unwrap() is provided for http.ResponseController (Go 1.20+) to access optional
// interfaces (http.Flusher, http.Hijacker, etc.) from the underlying writer.
type statusRecorder struct {
http.ResponseWriter
status int
wroteHeader bool
}

func (sr *statusRecorder) WriteHeader(code int) {
if !sr.wroteHeader && (code >= 200 || code == http.StatusSwitchingProtocols) {
sr.status = code
sr.wroteHeader = true
}
sr.ResponseWriter.WriteHeader(code)
Comment thread
ankurs marked this conversation as resolved.
}
Comment thread
ankurs marked this conversation as resolved.
Comment thread
ankurs marked this conversation as resolved.

func (sr *statusRecorder) Write(b []byte) (int, error) {
if !sr.wroteHeader {
sr.status = http.StatusOK
sr.wroteHeader = true
}
return sr.ResponseWriter.Write(b)
}

// Unwrap returns the underlying ResponseWriter so that http.ResponseController
// and middleware can access optional interfaces (http.Flusher, http.Hijacker, etc.).
func (sr *statusRecorder) Unwrap() http.ResponseWriter {
return sr.ResponseWriter
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// endSpan records the HTTP status code on the span, marks it as error for 5xx, and ends it.
func endSpan(span oteltrace.Span, rec *statusRecorder) {
span.SetAttributes(semconv.HTTPResponseStatusCode(rec.status))
if rec.status >= 500 {
span.SetStatus(codes.Error, http.StatusText(rec.status))
}
span.End()
}

// httpSpanAttributes returns the OTEL attributes for an incoming HTTP request,
// omitting empty-valued attributes (e.g. scheme behind a reverse proxy).
func httpSpanAttributes(r *http.Request) []attribute.KeyValue {
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
attrs := []attribute.KeyValue{
semconv.HTTPRequestMethodKey.String(r.Method),
semconv.URLPath(r.URL.Path),
semconv.ServerAddress(host),
}
Comment thread
ankurs marked this conversation as resolved.
if port != "" {
if p, err := strconv.Atoi(port); err == nil {
attrs = append(attrs, semconv.ServerPort(p))
}
}
Comment thread
ankurs marked this conversation as resolved.
if r.URL.RawQuery != "" {
attrs = append(attrs, semconv.URLQuery(r.URL.RawQuery))
Comment thread
ankurs marked this conversation as resolved.
}
if r.URL.Scheme != "" {
attrs = append(attrs, semconv.URLScheme(r.URL.Scheme))
}
Comment on lines +219 to +239
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the core.go file
find . -name "core.go" -type f | head -20

Repository: go-coldbrew/core

Length of output: 69


🏁 Script executed:

# Look for the httpSpanAttributes function to confirm the code matches the review
rg -n "func httpSpanAttributes" --type go -A 25

Repository: go-coldbrew/core

Length of output: 1131


🏁 Script executed:

# Check if there's any proxy header handling (X-Forwarded-Proto, etc.) in the codebase
rg -i "forwarded-proto|x-forwarded|proto" --type go

Repository: go-coldbrew/core

Length of output: 4179


🌐 Web query:

Go http.Request URL.Scheme behavior inbound requests net/http server

💡 Result:

In Go's net/http server, for inbound requests, http.Request.URL.Scheme is always an empty string (""). The server does not set it. The URL field in http.Request is populated by parsing the request-target from the HTTP Request-Line (stored in RequestURI) using url.ParseRequestURI (for HTTP/1.x via readRequest in request.go). Per RFC 7230 Section 5.3, request-targets in HTTP/1.x are typically relative URIs (absolute-path [ "?" query ]) without scheme or authority for non-authority forms (like origin-form used in direct requests). Thus, when parsed, the Scheme field remains empty, while Path and RawQuery are populated. Official documentation confirms: "For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For most requests, fields other than Path and RawQuery will be empty." (pkg.go.dev/net/http#Request) The scheme (http or https) is determined by the connection: check if r.TLS != nil ("https") or nil ("http"). The server sets r.TLS for TLS-enabled connections before invoking the handler. Behind proxies/load balancers, use headers like X-Forwarded-Proto (via Server.ProxyHeaders if configured). To construct full URL: scheme + "://" + r.Host + r.URL.RequestURI

Citations:


Derive url.scheme from the TLS connection rather than r.URL.Scheme.

On inbound server requests, r.URL.Scheme is always empty per Go's http.Request semantics (RFC 7230). Check r.TLS != nil to determine HTTPS vs HTTP instead, so spans always capture the actual scheme.

 	if r.URL.RawQuery != "" {
 		attrs = append(attrs, semconv.URLQuery(r.URL.RawQuery))
 	}
-	if r.URL.Scheme != "" {
-		attrs = append(attrs, semconv.URLScheme(r.URL.Scheme))
-	}
+	switch {
+	case r.URL.Scheme != "":
+		attrs = append(attrs, semconv.URLScheme(r.URL.Scheme))
+	case r.TLS != nil:
+		attrs = append(attrs, semconv.URLScheme("https"))
+	default:
+		attrs = append(attrs, semconv.URLScheme("http"))
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core.go` around lines 219 - 239, In httpSpanAttributes, replace the
conditional that reads r.URL.Scheme with logic that derives the URL scheme from
the TLS connection by checking r.TLS != nil (set "https" when non-nil, otherwise
"http") and append semconv.URLScheme(...) accordingly; update the
httpSpanAttributes function to use this TLS-based scheme determination so spans
capture the actual inbound request scheme.

return attrs
}

// tracingWrapper is a middleware that creates a new OTEL span for each incoming HTTP request.
// It extracts any propagated trace context from the request headers and, for non-filtered
// methods, starts a server span that is attached to the request context.
func tracingWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prop := otel.GetTextMapPropagator()
ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))

if interceptors.FilterMethodsFunc(ctx, r.URL.Path) {
var serverSpan oteltrace.Span
ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, "ServeHTTP",
ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, r.Method,
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
oteltrace.WithAttributes(
semconv.HTTPMethodKey.String(r.Method),
semconv.HTTPTargetKey.String(r.URL.RequestURI()),
),
oteltrace.WithAttributes(httpSpanAttributes(r)...),
)
Comment thread
ankurs marked this conversation as resolved.
r = r.WithContext(ctx)
defer serverSpan.End()
} else {
r = r.WithContext(ctx)
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
w = rec
defer endSpan(serverSpan, rec)
}

_, han := interceptors.NRHttpTracer("", h.ServeHTTP)
// add this info to log
ctx = r.Context()
ctx = options.AddToOptions(ctx, "", "")
ctx = loggers.AddToLogContext(ctx, "httpPath", r.URL.Path)
r = r.WithContext(ctx)
han(w, r)
han(w, r.WithContext(ctx))
Comment on lines +248 to +264
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "\.go$" | head -20

Repository: go-coldbrew/core

Length of output: 173


🏁 Script executed:

# Find the core.go file and examine the statusRecorder and endSpan implementations
fd -e go -name "core.go"

Repository: go-coldbrew/core

Length of output: 290


🏁 Script executed:

# Get the statusRecorder and endSpan definitions
rg "type statusRecorder" -A 20

Repository: go-coldbrew/core

Length of output: 702


🏁 Script executed:

# Find the endSpan function
rg "func endSpan" -A 15

Repository: go-coldbrew/core

Length of output: 772


🏁 Script executed:

# Look at the full context around lines 248-264
sed -n '240,270p' core.go

Repository: go-coldbrew/core

Length of output: 1302


🏁 Script executed:

# Check if there are any tests that verify panic handling
rg "panic" core_test.go -B 3 -A 10

Repository: go-coldbrew/core

Length of output: 42


🏁 Script executed:

# Look for any existing panic recovery in the codebase
rg "recover\(\)" -B 3 -A 3

Repository: go-coldbrew/core

Length of output: 832


🌐 Web query:

Go net/http Server panic recovery behavior writes 500 status code

💡 Result:

No, the standard Go net/http server does not write a 500 status code when recovering from a panic in a handler. Instead, it recovers from the panic (except for http.ErrAbortHandler), logs the panic details and stack trace to the server error log with a message like "http: panic serving [addr]: [error]\n[stack]", and then aborts the response by closing the network connection (for HTTP/1) or sending an HTTP/2 RST_STREAM. This typically results in the client receiving no response body and either a connection close (which may appear as an empty reply or connection reset, depending on the client) or an empty/incomplete response without a 500 status. This behavior is intentional to isolate the failed request and prevent it from crashing the server process, as documented in the net/http package and visible in the server.go source code around the defer recover in (*conn).serve. Custom middleware is commonly used to instead return a proper 500 Internal Server Error response, log the error, and continue.

Citations:


🏁 Script executed:

rg "NRHttpTracer" -B 2 -A 10

Repository: go-coldbrew/core

Length of output: 678


🏁 Script executed:

# Also search for its definition or implementation
rg "func NRHttpTracer" -A 20

Repository: go-coldbrew/core

Length of output: 42


Unhandled panics will be recorded as successful (200 status) in traces.

If the wrapped handler panics before writing a response, the defer executes during panic unwinding and records the span with the initial status (200). Since wroteHeader remains false, no actual response was sent to the client, but the trace will show a successful request. This misleads debugging and monitoring.

Suggested fix
 		if interceptors.FilterMethodsFunc(ctx, r.URL.Path) {
 			var serverSpan oteltrace.Span
 			ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, r.Method,
 				oteltrace.WithSpanKind(oteltrace.SpanKindServer),
 				oteltrace.WithAttributes(httpSpanAttributes(r)...),
 			)
 			rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
 			w = rec
-			defer endSpan(serverSpan, rec)
+			defer func() {
+				if p := recover(); p != nil {
+					if !rec.wroteHeader {
+						rec.status = http.StatusInternalServerError
+						rec.wroteHeader = true
+					}
+					endSpan(serverSpan, rec)
+					panic(p)
+				}
+				endSpan(serverSpan, rec)
+			}()
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core.go` around lines 248 - 264, The defer that calls endSpan(serverSpan,
rec) runs during panic unwinding and can record a 200 because
statusRecorder.wroteHeader is still false; change the wrapper so panics are
detected and treated as 500 before ending the span: replace the simple defer
endSpan(serverSpan, rec) with a recovery-aware wrapper around han that uses
defer+recover to check for a panic, and if recover() != nil and rec.wroteHeader
== false set rec.status = http.StatusInternalServerError (or otherwise ensure
endSpan treats "no header written" as 500) then call endSpan(serverSpan, rec)
and re-panic to preserve behavior; reference the statusRecorder struct, rec
variable, endSpan function, and the call to han(w, r.WithContext(ctx)) when
making this change.

})
}

// spanRouteMiddleware is a grpc-gateway middleware that updates the OTEL span
// name and http.route attribute with the matched route pattern after routing.
func spanRouteMiddleware(next runtime.HandlerFunc) runtime.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
if pattern, ok := runtime.HTTPPathPattern(r.Context()); ok {
span := oteltrace.SpanFromContext(r.Context())
span.SetName(r.Method + " " + pattern)
span.SetAttributes(semconv.HTTPRoute(pattern))
}
next(w, r, pathParams)
}
}

// getCustomHeaderMatcher returns a matcher that matches the given header and prefix
func getCustomHeaderMatcher(prefixes []string, header string) func(string) (string, bool) {
header = strings.ToLower(header)
Expand Down Expand Up @@ -239,6 +318,7 @@ func (c *cb) initHTTP(ctx context.Context) (*http.Server, error) {
),
runtime.WithMarshalerOption("application/proto", pMar),
runtime.WithMarshalerOption("application/protobuf", pMar),
runtime.WithMiddlewares(spanRouteMiddleware),
}

if c.config.UseJSONBuiltinMarshaller {
Expand Down
116 changes: 116 additions & 0 deletions core_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,122 @@ func TestTracingWrapper(t *testing.T) {
}
}

func TestTracingWrapper_StatusCodes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
handler http.HandlerFunc
wantStatus int
}{
{
name: "records 200",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
},
wantStatus: http.StatusOK,
},
{
name: "records 404",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
},
wantStatus: http.StatusNotFound,
},
{
name: "records 500",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
wantStatus: http.StatusInternalServerError,
},
{
name: "defaults to 200 when WriteHeader not called",
handler: func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "ok")
},
wantStatus: http.StatusOK,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
wrapped := tracingWrapper(tt.handler)
req := httptest.NewRequest("GET", "/api/test", nil)
w := httptest.NewRecorder()
wrapped.ServeHTTP(w, req)
if w.Code != tt.wantStatus {
t.Fatalf("expected %d, got %d", tt.wantStatus, w.Code)
}
})
}
}
Comment thread
ankurs marked this conversation as resolved.

func TestStatusRecorder(t *testing.T) {
t.Parallel()

t.Run("captures explicit status", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
rec.WriteHeader(http.StatusBadGateway)
if rec.status != http.StatusBadGateway {
t.Fatalf("expected %d, got %d", http.StatusBadGateway, rec.status)
}
if w.Code != http.StatusBadGateway {
t.Fatalf("underlying writer expected %d, got %d", http.StatusBadGateway, w.Code)
}
})

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

statusRecorder has special handling for 1xx (skip) and 101 Switching Protocols (terminal) in WriteHeader, but the tests only cover >=200 statuses. Add a test case that exercises WriteHeader(http.StatusSwitchingProtocols) (and ideally a 1xx-then-200 sequence) to prevent regressions in this branch logic.

Suggested change
t.Run("switching protocols is terminal", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
rec.WriteHeader(http.StatusSwitchingProtocols)
// A later attempt to change the status should be ignored once 101 is written.
rec.WriteHeader(http.StatusBadGateway)
if rec.status != http.StatusSwitchingProtocols {
t.Fatalf("expected recorder status %d, got %d", http.StatusSwitchingProtocols, rec.status)
}
if w.Code != http.StatusSwitchingProtocols {
t.Fatalf("underlying writer expected %d, got %d", http.StatusSwitchingProtocols, w.Code)
}
})
t.Run("informational status followed by final status", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
// First write a non-101 informational status; this should be skipped.
rec.WriteHeader(102)
// Then write a final status, which should be recorded and sent.
rec.WriteHeader(http.StatusOK)
if rec.status != http.StatusOK {
t.Fatalf("expected recorder status %d after informational, got %d", http.StatusOK, rec.status)
}
if w.Code != http.StatusOK {
t.Fatalf("underlying writer expected %d after informational, got %d", http.StatusOK, w.Code)
}
})

Copilot uses AI. Check for mistakes.
t.Run("unwrap returns underlying writer", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
rec := &statusRecorder{ResponseWriter: w}
if rec.Unwrap() != w {
t.Fatal("Unwrap did not return underlying ResponseWriter")
}
})
}

func TestSpanRouteMiddleware(t *testing.T) {
t.Parallel()

t.Run("calls next handler", func(t *testing.T) {
t.Parallel()
called := false
next := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
called = true
w.WriteHeader(http.StatusOK)
}
wrapped := spanRouteMiddleware(next)
req := httptest.NewRequest("GET", "/api/v1/rules", nil)
w := httptest.NewRecorder()
wrapped(w, req, nil)
if !called {
t.Fatal("expected next handler to be called")
}
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
})

t.Run("passes path params through", func(t *testing.T) {
t.Parallel()
var gotParams map[string]string
next := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
gotParams = pathParams
}
wrapped := spanRouteMiddleware(next)
params := map[string]string{"id": "123"}
req := httptest.NewRequest("GET", "/api/v1/rules/123", nil)
w := httptest.NewRecorder()
wrapped(w, req, params)
if gotParams["id"] != "123" {
t.Fatalf("expected path param id=123, got %v", gotParams)
}
})
}

func TestGetCustomHeaderMatcher_EmptyPrefixes(t *testing.T) {
t.Parallel()
matcher := getCustomHeaderMatcher(nil, "X-Trace-Id")
Expand Down
31 changes: 25 additions & 6 deletions initializers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"strings"
"sync"
"syscall"
Expand All @@ -23,13 +25,14 @@ import (
"github.com/opentracing/opentracing-go"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otelBridge "go.opentelemetry.io/otel/bridge/opentracing"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.uber.org/automaxprocs/maxprocs"
"google.golang.org/grpc/encoding"
_ "google.golang.org/grpc/encoding/gzip"
Expand Down Expand Up @@ -210,12 +213,28 @@ func SetupOpenTelemetry(config OTLPConfig) error {
}

d := resource.Default()
attrs := []attribute.KeyValue{
semconv.ServiceName(config.ServiceName),
semconv.ServiceVersion(config.ServiceVersion),
}
if bi, ok := debug.ReadBuildInfo(); ok {
attrs = append(attrs,
semconv.ProcessExecutableName(filepath.Base(os.Args[0])),
semconv.ProcessRuntimeVersion(bi.GoVersion),
)
for _, s := range bi.Settings {
switch s.Key {
case "vcs.revision":
attrs = append(attrs, semconv.VCSRefHeadRevision(s.Value))
case "vcs.time":
attrs = append(attrs, attribute.String("vcs.time", s.Value))
case "vcs.modified":
attrs = append(attrs, attribute.Bool("vcs.modified", s.Value == "true"))
}
}
}
res, err := resource.New(context.Background(),
resource.WithAttributes(
// the service name used to display traces in backends
semconv.ServiceNameKey.String(config.ServiceName),
semconv.ServiceVersionKey.String(config.ServiceVersion),
),
resource.WithAttributes(attrs...),
)
if err != nil {
log.Error(context.Background(), "msg", "creating OTLP resource", "err", err)
Expand Down
Loading