137 lines
3.7 KiB
Go
137 lines
3.7 KiB
Go
// Package httpattr collapses the many attribute spellings used for "which
|
|
// HTTP endpoint was this" (OTel stable semconv, legacy semconv, nginx, Go
|
|
// net/http, browser RUM) into one Endpoint, so spans from different
|
|
// instrumentations can be compared and joined.
|
|
package httpattr
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// Endpoint is the normalized identity of one HTTP request.
|
|
type Endpoint struct {
|
|
Method string // upper-case, e.g. "GET"
|
|
Host string // host without port, lower-case; "" if unknown
|
|
Path string // concrete path, no query/fragment, no trailing slash
|
|
Route string // instrumentation-provided template ("/orders/:id"), may be ""
|
|
}
|
|
|
|
// Key is the join key between a client-side (RUM) request and a server-side
|
|
// span: method plus the concrete path. Route is deliberately not part of it -
|
|
// a browser only knows the concrete URL.
|
|
func (e Endpoint) Key() string {
|
|
if e.Path == "" {
|
|
return ""
|
|
}
|
|
return e.Method + " " + e.Path
|
|
}
|
|
|
|
// Lookup returns the first non-empty value among keys.
|
|
type Lookup func(keys ...string) string
|
|
|
|
// Extract reads an Endpoint out of span/log attributes.
|
|
//
|
|
// Sources, in order of preference:
|
|
//
|
|
// method: http.request.method, http.method
|
|
// path: url.path, http.target (path+query), url.full, http.url
|
|
// host: server.address, http.host, net.host.name, url.full/http.url host
|
|
// route: http.route
|
|
//
|
|
// nginx's OTel module emits the stable keys; older nginx/Go/Java agents emit
|
|
// http.target/http.url; browsers only know url.full (absolute or relative).
|
|
func Extract(get Lookup) Endpoint {
|
|
e := Endpoint{
|
|
Method: strings.ToUpper(get("http.request.method", "http.method")),
|
|
Route: get("http.route"),
|
|
}
|
|
e.Host = stripPort(strings.ToLower(get("server.address", "http.host", "net.host.name", "server.host")))
|
|
if p := get("url.path"); p != "" {
|
|
e.Path = CleanPath(p)
|
|
} else if t := get("http.target"); t != "" {
|
|
e.Path = CleanPath(t)
|
|
}
|
|
if full := get("url.full", "http.url"); full != "" {
|
|
host, path := SplitURL(full)
|
|
if e.Path == "" {
|
|
e.Path = path
|
|
}
|
|
if e.Host == "" {
|
|
e.Host = host
|
|
}
|
|
}
|
|
// Some instrumentations report the router mount ("/") as the route.
|
|
if e.Route == "/" && e.Path != "" && e.Path != "/" {
|
|
e.Route = ""
|
|
}
|
|
return e
|
|
}
|
|
|
|
// SplitURL splits an absolute or relative URL into (host without port, clean path).
|
|
func SplitURL(raw string) (host, path string) {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return "", CleanPath(raw)
|
|
}
|
|
return stripPort(strings.ToLower(u.Host)), CleanPath(u.Path)
|
|
}
|
|
|
|
// CleanPath drops query/fragment and a trailing slash, and guarantees a
|
|
// leading slash. "" stays "".
|
|
func CleanPath(p string) string {
|
|
if i := strings.IndexAny(p, "?#"); i >= 0 {
|
|
p = p[:i]
|
|
}
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
if !strings.HasPrefix(p, "/") {
|
|
p = "/" + p
|
|
}
|
|
if len(p) > 1 {
|
|
p = strings.TrimRight(p, "/")
|
|
if p == "" {
|
|
p = "/"
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// StripPrefix removes a reverse-proxy mount prefix (e.g. nginx
|
|
// `location /api/ { proxy_pass http://backend/; }` makes the browser see
|
|
// /api/orders while the backend sees /orders). The longest matching prefix wins.
|
|
func StripPrefix(path string, prefixes []string) string {
|
|
best := ""
|
|
for _, p := range prefixes {
|
|
p = CleanPath(p)
|
|
if p == "" || p == "/" {
|
|
continue
|
|
}
|
|
if (path == p || strings.HasPrefix(path, p+"/")) && len(p) > len(best) {
|
|
best = p
|
|
}
|
|
}
|
|
if best == "" {
|
|
return path
|
|
}
|
|
if rest := path[len(best):]; rest != "" {
|
|
return rest
|
|
}
|
|
return "/"
|
|
}
|
|
|
|
func stripPort(host string) string {
|
|
if strings.HasPrefix(host, "[") { // IPv6 literal
|
|
if i := strings.Index(host, "]"); i > 0 {
|
|
return host[:i+1]
|
|
}
|
|
return host
|
|
}
|
|
if i := strings.LastIndexByte(host, ':'); i >= 0 {
|
|
return host[:i]
|
|
}
|
|
return host
|
|
}
|