httpconv.go

  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}