1// Copyright The OpenTelemetry Authors
2// SPDX-License-Identifier: Apache-2.0
3
4package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
5
6import (
7 "fmt"
8 "net/http"
9 "reflect"
10 "strconv"
11 "strings"
12
13 "go.opentelemetry.io/otel/attribute"
14 semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0"
15)
16
17type newHTTPServer struct{}
18
19// TraceRequest returns trace attributes for an HTTP request received by a
20// server.
21//
22// The server must be the primary server name if it is known. For example this
23// would be the ServerName directive
24// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache
25// server, and the server_name directive
26// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an
27// nginx server. More generically, the primary server name would be the host
28// header value that matches the default virtual host of an HTTP server. It
29// should include the host identifier and if a port is used to route to the
30// server that port identifier should be included as an appropriate port
31// suffix.
32//
33// If the primary server name is not known, server should be an empty string.
34// The req Host will be used to determine the server instead.
35func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue {
36 count := 3 // ServerAddress, Method, Scheme
37
38 var host string
39 var p int
40 if server == "" {
41 host, p = splitHostPort(req.Host)
42 } else {
43 // Prioritize the primary server name.
44 host, p = splitHostPort(server)
45 if p < 0 {
46 _, p = splitHostPort(req.Host)
47 }
48 }
49
50 hostPort := requiredHTTPPort(req.TLS != nil, p)
51 if hostPort > 0 {
52 count++
53 }
54
55 method, methodOriginal := n.method(req.Method)
56 if methodOriginal != (attribute.KeyValue{}) {
57 count++
58 }
59
60 scheme := n.scheme(req.TLS != nil)
61
62 if peer, peerPort := splitHostPort(req.RemoteAddr); peer != "" {
63 // The Go HTTP server sets RemoteAddr to "IP:port", this will not be a
64 // file-path that would be interpreted with a sock family.
65 count++
66 if peerPort > 0 {
67 count++
68 }
69 }
70
71 useragent := req.UserAgent()
72 if useragent != "" {
73 count++
74 }
75
76 clientIP := serverClientIP(req.Header.Get("X-Forwarded-For"))
77 if clientIP != "" {
78 count++
79 }
80
81 if req.URL != nil && req.URL.Path != "" {
82 count++
83 }
84
85 protoName, protoVersion := netProtocol(req.Proto)
86 if protoName != "" && protoName != "http" {
87 count++
88 }
89 if protoVersion != "" {
90 count++
91 }
92
93 attrs := make([]attribute.KeyValue, 0, count)
94 attrs = append(attrs,
95 semconvNew.ServerAddress(host),
96 method,
97 scheme,
98 )
99
100 if hostPort > 0 {
101 attrs = append(attrs, semconvNew.ServerPort(hostPort))
102 }
103 if methodOriginal != (attribute.KeyValue{}) {
104 attrs = append(attrs, methodOriginal)
105 }
106
107 if peer, peerPort := splitHostPort(req.RemoteAddr); peer != "" {
108 // The Go HTTP server sets RemoteAddr to "IP:port", this will not be a
109 // file-path that would be interpreted with a sock family.
110 attrs = append(attrs, semconvNew.NetworkPeerAddress(peer))
111 if peerPort > 0 {
112 attrs = append(attrs, semconvNew.NetworkPeerPort(peerPort))
113 }
114 }
115
116 if useragent := req.UserAgent(); useragent != "" {
117 attrs = append(attrs, semconvNew.UserAgentOriginal(useragent))
118 }
119
120 if clientIP != "" {
121 attrs = append(attrs, semconvNew.ClientAddress(clientIP))
122 }
123
124 if req.URL != nil && req.URL.Path != "" {
125 attrs = append(attrs, semconvNew.URLPath(req.URL.Path))
126 }
127
128 if protoName != "" && protoName != "http" {
129 attrs = append(attrs, semconvNew.NetworkProtocolName(protoName))
130 }
131 if protoVersion != "" {
132 attrs = append(attrs, semconvNew.NetworkProtocolVersion(protoVersion))
133 }
134
135 return attrs
136}
137
138func (n newHTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) {
139 if method == "" {
140 return semconvNew.HTTPRequestMethodGet, attribute.KeyValue{}
141 }
142 if attr, ok := methodLookup[method]; ok {
143 return attr, attribute.KeyValue{}
144 }
145
146 orig := semconvNew.HTTPRequestMethodOriginal(method)
147 if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
148 return attr, orig
149 }
150 return semconvNew.HTTPRequestMethodGet, orig
151}
152
153func (n newHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive
154 if https {
155 return semconvNew.URLScheme("https")
156 }
157 return semconvNew.URLScheme("http")
158}
159
160// TraceResponse returns trace attributes for telemetry from an HTTP response.
161//
162// If any of the fields in the ResponseTelemetry are not set the attribute will be omitted.
163func (n newHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue {
164 var count int
165
166 if resp.ReadBytes > 0 {
167 count++
168 }
169 if resp.WriteBytes > 0 {
170 count++
171 }
172 if resp.StatusCode > 0 {
173 count++
174 }
175
176 attributes := make([]attribute.KeyValue, 0, count)
177
178 if resp.ReadBytes > 0 {
179 attributes = append(attributes,
180 semconvNew.HTTPRequestBodySize(int(resp.ReadBytes)),
181 )
182 }
183 if resp.WriteBytes > 0 {
184 attributes = append(attributes,
185 semconvNew.HTTPResponseBodySize(int(resp.WriteBytes)),
186 )
187 }
188 if resp.StatusCode > 0 {
189 attributes = append(attributes,
190 semconvNew.HTTPResponseStatusCode(resp.StatusCode),
191 )
192 }
193
194 return attributes
195}
196
197// Route returns the attribute for the route.
198func (n newHTTPServer) Route(route string) attribute.KeyValue {
199 return semconvNew.HTTPRoute(route)
200}
201
202type newHTTPClient struct{}
203
204// RequestTraceAttrs returns trace attributes for an HTTP request made by a client.
205func (n newHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue {
206 /*
207 below attributes are returned:
208 - http.request.method
209 - http.request.method.original
210 - url.full
211 - server.address
212 - server.port
213 - network.protocol.name
214 - network.protocol.version
215 */
216 numOfAttributes := 3 // URL, server address, proto, and method.
217
218 var urlHost string
219 if req.URL != nil {
220 urlHost = req.URL.Host
221 }
222 var requestHost string
223 var requestPort int
224 for _, hostport := range []string{urlHost, req.Header.Get("Host")} {
225 requestHost, requestPort = splitHostPort(hostport)
226 if requestHost != "" || requestPort > 0 {
227 break
228 }
229 }
230
231 eligiblePort := requiredHTTPPort(req.URL != nil && req.URL.Scheme == "https", requestPort)
232 if eligiblePort > 0 {
233 numOfAttributes++
234 }
235 useragent := req.UserAgent()
236 if useragent != "" {
237 numOfAttributes++
238 }
239
240 protoName, protoVersion := netProtocol(req.Proto)
241 if protoName != "" && protoName != "http" {
242 numOfAttributes++
243 }
244 if protoVersion != "" {
245 numOfAttributes++
246 }
247
248 method, originalMethod := n.method(req.Method)
249 if originalMethod != (attribute.KeyValue{}) {
250 numOfAttributes++
251 }
252
253 attrs := make([]attribute.KeyValue, 0, numOfAttributes)
254
255 attrs = append(attrs, method)
256 if originalMethod != (attribute.KeyValue{}) {
257 attrs = append(attrs, originalMethod)
258 }
259
260 var u string
261 if req.URL != nil {
262 // Remove any username/password info that may be in the URL.
263 userinfo := req.URL.User
264 req.URL.User = nil
265 u = req.URL.String()
266 // Restore any username/password info that was removed.
267 req.URL.User = userinfo
268 }
269 attrs = append(attrs, semconvNew.URLFull(u))
270
271 attrs = append(attrs, semconvNew.ServerAddress(requestHost))
272 if eligiblePort > 0 {
273 attrs = append(attrs, semconvNew.ServerPort(eligiblePort))
274 }
275
276 if protoName != "" && protoName != "http" {
277 attrs = append(attrs, semconvNew.NetworkProtocolName(protoName))
278 }
279 if protoVersion != "" {
280 attrs = append(attrs, semconvNew.NetworkProtocolVersion(protoVersion))
281 }
282
283 return attrs
284}
285
286// ResponseTraceAttrs returns trace attributes for an HTTP response made by a client.
287func (n newHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue {
288 /*
289 below attributes are returned:
290 - http.response.status_code
291 - error.type
292 */
293 var count int
294 if resp.StatusCode > 0 {
295 count++
296 }
297
298 if isErrorStatusCode(resp.StatusCode) {
299 count++
300 }
301
302 attrs := make([]attribute.KeyValue, 0, count)
303 if resp.StatusCode > 0 {
304 attrs = append(attrs, semconvNew.HTTPResponseStatusCode(resp.StatusCode))
305 }
306
307 if isErrorStatusCode(resp.StatusCode) {
308 errorType := strconv.Itoa(resp.StatusCode)
309 attrs = append(attrs, semconvNew.ErrorTypeKey.String(errorType))
310 }
311 return attrs
312}
313
314func (n newHTTPClient) ErrorType(err error) attribute.KeyValue {
315 t := reflect.TypeOf(err)
316 var value string
317 if t.PkgPath() == "" && t.Name() == "" {
318 // Likely a builtin type.
319 value = t.String()
320 } else {
321 value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
322 }
323
324 if value == "" {
325 return semconvNew.ErrorTypeOther
326 }
327
328 return semconvNew.ErrorTypeKey.String(value)
329}
330
331func (n newHTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) {
332 if method == "" {
333 return semconvNew.HTTPRequestMethodGet, attribute.KeyValue{}
334 }
335 if attr, ok := methodLookup[method]; ok {
336 return attr, attribute.KeyValue{}
337 }
338
339 orig := semconvNew.HTTPRequestMethodOriginal(method)
340 if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
341 return attr, orig
342 }
343 return semconvNew.HTTPRequestMethodGet, orig
344}
345
346func isErrorStatusCode(code int) bool {
347 return code >= 400 || code < 100
348}