Skip to content

Commit a061784

Browse files
authored
fix: missing dot between header attributes (#2)
* fix: missing dot between header attributes * follow semconv compliance
1 parent 0af9e5f commit a061784

File tree

3 files changed

+127
-45
lines changed

3 files changed

+127
-45
lines changed

middleware.go

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"net/http"
99
"runtime/debug"
1010
"slices"
11-
"strconv"
1211
"strings"
1312
"time"
1413

@@ -17,6 +16,7 @@ import (
1716
"go.opentelemetry.io/otel/codes"
1817
"go.opentelemetry.io/otel/metric"
1918
"go.opentelemetry.io/otel/propagation"
19+
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
2020
"go.opentelemetry.io/otel/trace"
2121
)
2222

@@ -102,41 +102,31 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
102102
ctx := r.Context()
103103
span := trace.SpanFromContext(ctx)
104104

105-
hostName, rawPort, _ := strings.Cut(r.Host, ":")
106-
port := 80
105+
urlScheme := r.URL.Scheme
106+
if urlScheme == "" {
107+
urlScheme = "http"
108+
}
107109

108-
if rawPort != "" {
109-
p, err := strconv.Atoi(rawPort)
110-
if err == nil {
111-
port = p
112-
}
113-
} else if strings.HasPrefix(r.URL.Scheme, "https") {
110+
hostName, port, _ := SplitHostPort(r.Host)
111+
112+
switch {
113+
case port > 0:
114+
case urlScheme == "https":
114115
port = 443
116+
default:
117+
port = 80
115118
}
116119

117120
metricAttrs := []attribute.KeyValue{
118-
attribute.String("http.request.method", r.Method),
119-
attribute.String("url.scheme", r.URL.Scheme),
120-
attribute.String("server.address", hostName),
121-
attribute.Int("server.port", port),
121+
{
122+
Key: semconv.HTTPRequestMethodKey,
123+
Value: attribute.StringValue(r.Method),
124+
},
125+
semconv.URLScheme(urlScheme),
126+
semconv.ServerAddress(hostName),
127+
semconv.ServerPort(port),
128+
semconv.ClientAddress(r.RemoteAddr),
122129
}
123-
requestPathAttr := attribute.String("http.request.path", r.URL.Path)
124-
125-
if !tm.Options.HighCardinalityMetricDisabled {
126-
metricAttrs = append(metricAttrs, requestPathAttr)
127-
}
128-
129-
activeRequestsAttrSet := metric.WithAttributeSet(attribute.NewSet(metricAttrs...))
130-
131-
tm.ActiveRequestsMetric.Add(ctx, 1, activeRequestsAttrSet)
132-
133-
metricAttrs = append(
134-
metricAttrs,
135-
attribute.String(
136-
"network.protocol.version",
137-
fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor),
138-
),
139-
)
140130

141131
if !slices.Contains(tm.Options.DebugPaths, strings.ToLower(r.URL.Path)) {
142132
ctx, span = tm.Exporters.Tracer.Start(
@@ -157,7 +147,37 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
157147
// Add HTTP semantic attributes to the server span
158148
// See: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-semantic-conventions
159149
span.SetAttributes(metricAttrs...)
160-
span.SetAttributes(requestPathAttr)
150+
151+
if !tm.Options.HighCardinalityMetricDisabled {
152+
metricAttrs = append(metricAttrs, attribute.String("http.request.path", r.URL.Path))
153+
}
154+
155+
activeRequestsAttrSet := metric.WithAttributeSet(attribute.NewSet(metricAttrs...))
156+
157+
tm.ActiveRequestsMetric.Add(ctx, 1, activeRequestsAttrSet)
158+
159+
protocolAttr := semconv.NetworkProtocolVersion(fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor))
160+
161+
metricAttrs = append(
162+
metricAttrs,
163+
protocolAttr,
164+
)
165+
166+
span.SetAttributes(
167+
protocolAttr,
168+
semconv.URLFull(r.URL.String()),
169+
semconv.UserAgentOriginal(r.UserAgent()),
170+
)
171+
172+
peer, peerPort, _ := SplitHostPort(r.RemoteAddr)
173+
174+
if peer != "" {
175+
span.SetAttributes(semconv.NetworkPeerAddress(peer))
176+
177+
if peerPort > 0 {
178+
span.SetAttributes(semconv.NetworkPeerPort(peerPort))
179+
}
180+
}
161181

162182
requestBodySize := r.ContentLength
163183
requestLogHeaders := NewTelemetryHeaders(r.Header, tm.Options.AllowedRequestHeaders...)
@@ -198,12 +218,10 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
198218
activeRequestsAttrSet,
199219
)
200220

201-
statusCodeAttr := attribute.Int(
202-
"http.response.status_code",
203-
statusCode,
204-
)
205-
latency := time.Since(start).Seconds()
221+
statusCodeAttr := semconv.HTTPResponseStatusCode(statusCode)
222+
span.SetAttributes(statusCodeAttr)
206223

224+
latency := time.Since(start).Seconds()
207225
responseLogData["status"] = statusCode
208226

209227
logAttrs := []any{
@@ -215,7 +233,7 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
215233
if err != nil {
216234
stack := string(debug.Stack())
217235
logAttrs = append(logAttrs, slog.Any("error", err), slog.String("stacktrace", stack))
218-
span.SetAttributes(statusCodeAttr, attribute.String("stacktrace", stack))
236+
span.SetAttributes(semconv.ExceptionStacktrace(stack))
219237
}
220238

221239
metricAttrs = append(metricAttrs, statusCodeAttr)
@@ -265,7 +283,7 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
265283

266284
if requestBodySize > 0 {
267285
requestLogData["size"] = requestBodySize
268-
span.SetAttributes(attribute.Int64("http.request.body.size", requestBodySize))
286+
span.SetAttributes(semconv.HTTPRequestBodySize(int(requestBodySize)))
269287
}
270288

271289
defer func() {
@@ -284,9 +302,9 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
284302

285303
errBytes, jsonErr := json.Marshal(err)
286304
if jsonErr != nil {
287-
span.SetAttributes(attribute.String("error", fmt.Sprintf("%v", err)))
305+
span.SetAttributes(attribute.String("exception.error", fmt.Sprintf("%v", err)))
288306
} else {
289-
span.SetAttributes(attribute.String("error", string(errBytes)))
307+
span.SetAttributes(attribute.String("exception.error", string(errBytes)))
290308
}
291309
}
292310
}()
@@ -300,7 +318,7 @@ func (tm *tracingMiddleware) ServeHTTP( //nolint:gocognit,cyclop,funlen,maintidx
300318
responseLogData["size"] = ww.BytesWritten()
301319
responseLogData["headers"] = responseLogHeaders
302320

303-
span.SetAttributes(attribute.Int("http.response.body.size", ww.BytesWritten()))
321+
span.SetAttributes(semconv.HTTPResponseBodySize(ww.BytesWritten()))
304322
SetSpanHeaderAttributes(span, "http.response.header", responseLogHeaders)
305323

306324
// skip printing very large responses.

utils.go

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package gotel
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"log/slog"
10+
"net"
911
"net/http"
1012
"regexp"
1113
"slices"
14+
"strconv"
1215
"strings"
1316

1417
"github.com/google/uuid"
@@ -23,6 +26,21 @@ const (
2326
contentTypeHeader = "Content-Type"
2427
)
2528

29+
var excludedSpanHeaderAttributes = map[string]bool{
30+
"baggage": true,
31+
"traceparent": true,
32+
"traceresponse": true,
33+
"tracestate": true,
34+
"x-b3-sampled": true,
35+
"x-b3-spanid": true,
36+
"x-b3-traceid": true,
37+
"x-b3-parentspanid": true,
38+
"x-b3-flags": true,
39+
"b3": true,
40+
}
41+
42+
var errInvalidHostPort = errors.New("invalid host port")
43+
2644
// SetSpanHeaderAttributes sets header attributes to the otel span.
2745
func SetSpanHeaderAttributes(
2846
span trace.Span,
@@ -33,8 +51,13 @@ func SetSpanHeaderAttributes(
3351
allowedHeadersLength := len(allowedHeaders)
3452

3553
for key, values := range headers {
36-
if allowedHeadersLength == 0 || slices.Contains(allowedHeaders, strings.ToLower(key)) {
37-
span.SetAttributes(attribute.StringSlice(prefix+strings.ToLower(key), values))
54+
lowerKey := strings.ToLower(key)
55+
56+
if (allowedHeadersLength == 0 && !excludedSpanHeaderAttributes[lowerKey]) ||
57+
(allowedHeadersLength > 0 && slices.Contains(allowedHeaders, lowerKey)) {
58+
span.SetAttributes(
59+
attribute.StringSlice(fmt.Sprintf("%s.%s", prefix, lowerKey), values),
60+
)
3861
}
3962
}
4063
}
@@ -95,10 +118,51 @@ func MaskString(input string) string {
95118
case inputLength < 12:
96119
return input[0:1] + strings.Repeat("*", inputLength-1)
97120
default:
98-
return input[0:3] + strings.Repeat("*", 7) + fmt.Sprintf("(%d)", inputLength)
121+
return input[0:2] + strings.Repeat("*", 8) + fmt.Sprintf("(%d)", inputLength)
99122
}
100123
}
101124

125+
// SplitHostPort splits a network address hostport of the form "host",
126+
// "host%zone", "[host]", "[host%zone]", "host:port", "host%zone:port",
127+
// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and
128+
// port.
129+
//
130+
// An empty host is returned if it is not provided or unparsable. A negative
131+
// port is returned if it is not provided or unparsable.
132+
func SplitHostPort(hostport string) (string, int, error) {
133+
port := -1
134+
135+
if strings.HasPrefix(hostport, "[") {
136+
addrEnd := strings.LastIndex(hostport, "]")
137+
if addrEnd < 0 {
138+
// Invalid hostport.
139+
return "", port, errInvalidHostPort
140+
}
141+
142+
if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 {
143+
host := hostport[1:addrEnd]
144+
145+
return host, port, nil
146+
}
147+
} else {
148+
if i := strings.LastIndex(hostport, ":"); i < 0 {
149+
return hostport, port, nil
150+
}
151+
}
152+
153+
host, pStr, err := net.SplitHostPort(hostport)
154+
if err != nil {
155+
return host, port, err
156+
}
157+
158+
p, err := strconv.ParseUint(pStr, 10, 16)
159+
if err != nil {
160+
return "", port, err
161+
}
162+
163+
return host, int(p), err
164+
}
165+
102166
// returns the value or default one if value is empty.
103167
func getDefault[T comparable](value T, defaultValue T) T {
104168
var empty T

utils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestNewTelemetryHeaders(t *testing.T) {
2626
},
2727
Expected: http.Header{
2828
"Content-Type": []string{"application/json"},
29-
"Authorization": []string{"Bea*******(65)"},
29+
"Authorization": []string{"Be********(65)"},
3030
"Api-Key": []string{"******"},
3131
"Secret-Key": []string{"s*********"},
3232
},

0 commit comments

Comments
 (0)