8
"maps"
9
"net"
10
"net/http"
11
+ "slices"
12
+ "strings"
13
"time"
14
15
"github.com/ipfs/boxo/blockservice"
27
"github.com/ipfs/kubo/core/node"
28
"github.com/libp2p/go-libp2p/core/routing"
29
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
30
+ "go.opentelemetry.io/otel/attribute"
31
)
32
33
func GatewayOption(paths ...string) ServeOption {
44
45
handler := gateway.NewHandler(config, backend)
46
handler = gateway.NewHeaders(headers).ApplyCors().Wrap(handler)
44
- handler = otelhttp.NewHandler(handler, "Gateway")
47
+ var otelOpts []otelhttp.Option
48
+ if fn := newServerDomainAttrFn(n); fn != nil {
49
+ otelOpts = append(otelOpts, otelhttp.WithMetricAttributesFn(fn))
50
+ }
51
+ handler = otelhttp.NewHandler(handler, "Gateway", otelOpts...)
52
53
for _, p := range paths {
54
mux.Handle(p+"/", handler)
75
var handler http.Handler
76
handler = gateway.NewHostnameHandler(config, backend, childMux)
77
handler = gateway.NewHeaders(headers).ApplyCors().Wrap(handler)
71
- handler = otelhttp.NewHandler(handler, "HostnameGateway")
78
+ var otelOpts []otelhttp.Option
79
+ if fn := newServerDomainAttrFn(n); fn != nil {
80
+ otelOpts = append(otelOpts, otelhttp.WithMetricAttributesFn(fn))
81
+ }
82
+ handler = otelhttp.NewHandler(handler, "HostnameGateway", otelOpts...)
83
84
mux.Handle("/", handler)
85
return childMux, nil
131
}
132
133
handler := gateway.NewHandler(gwConfig, &offlineGatewayErrWrapper{gwimpl: backend})
123
- handler = otelhttp.NewHandler(handler, "Libp2p-Gateway")
134
+ handler = otelhttp.NewHandler(handler, "Libp2p-Gateway",
135
+ otelhttp.WithMetricAttributesFn(staticServerDomainAttrFn("libp2p")),
136
+ )
137
138
mux.Handle("/ipfs/", handler)
139
265
266
var defaultPaths = []string{"/ipfs/", "/ipns/", "/p2p/"}
267
268
+// serverDomainAttrKey is the OTel attribute key for the logical server domain.
269
+// It replaces the high-cardinality server.address attribute (dropped by the
270
+// View in cmd/ipfs/kubo/daemon.go) with a bounded set of values: configured
271
+// Gateway.PublicGateways suffixes, "localhost", "loopback", "api", "libp2p",
272
+// or "other".
273
+var serverDomainAttrKey = attribute.Key("server.domain")
274
+
275
+// staticServerDomainAttrFn returns a MetricAttributesFn that always returns
276
+// a fixed server.domain value. Use for handlers where the domain is known
277
+// statically (e.g. "api", "libp2p") to keep the label set consistent across
278
+// all http_server_* metrics.
279
+func staticServerDomainAttrFn(domain string) func(*http.Request) []attribute.KeyValue {
280
+ attrs := []attribute.KeyValue{serverDomainAttrKey.String(domain)}
281
+ return func(*http.Request) []attribute.KeyValue { return attrs }
282
+}
283
+
284
+// newServerDomainAttrFn returns an otelhttp.WithMetricAttributesFn callback
285
+// that adds a server.domain attribute grouping requests by their matching
286
+// Gateway.PublicGateways hostname suffix (e.g. "dweb.link", "ipfs.io").
287
+// Requests that don't match any configured gateway get "other".
288
+//
289
+// All return values are pre-allocated at setup time so the per-request
290
+// closure is zero-allocation.
291
+func newServerDomainAttrFn(n *core.IpfsNode) func(*http.Request) []attribute.KeyValue {
292
+ cfg, err := n.Repo.Config()
293
+ if err != nil {
294
+ return nil
295
+ }
296
+
297
+ // Collect non-nil gateway domain suffixes, sorted longest-first
298
+ // so more-specific suffixes match before shorter ones.
299
+ // Strip ports from keys to match boxo's fallback behavior
300
+ // (boxo tries exact match with port, then strips port and retries).
301
+ seen := make(map[string]struct{}, len(cfg.Gateway.PublicGateways))
302
+ suffixes := make([]string, 0, len(cfg.Gateway.PublicGateways))
303
+ for hostname, gw := range cfg.Gateway.PublicGateways {
304
+ if gw == nil {
305
+ continue
306
+ }
307
+ if h, _, err := net.SplitHostPort(hostname); err == nil {
308
+ hostname = h
309
+ }
310
+ if _, ok := seen[hostname]; ok {
311
+ continue
312
+ }
313
+ seen[hostname] = struct{}{}
314
+ suffixes = append(suffixes, hostname)
315
+ }
316
+ slices.SortFunc(suffixes, func(a, b string) int {
317
+ return len(b) - len(a)
318
+ })
319
+
320
+ // Pre-allocate attribute slices so the per-request closure only returns
321
+ // existing slices and does not allocate.
322
+ suffixAttrs := make([][]attribute.KeyValue, len(suffixes))
323
+ for i, s := range suffixes {
324
+ suffixAttrs[i] = []attribute.KeyValue{serverDomainAttrKey.String(s)}
325
+ }
326
+ localhostAttr := []attribute.KeyValue{serverDomainAttrKey.String("localhost")}
327
+ loopbackAttr := []attribute.KeyValue{serverDomainAttrKey.String("loopback")}
328
+ otherAttr := []attribute.KeyValue{serverDomainAttrKey.String("other")}
329
+
330
+ return func(r *http.Request) []attribute.KeyValue {
331
+ host := r.Host
332
+ if h, _, err := net.SplitHostPort(host); err == nil {
333
+ host = h
334
+ }
335
+
336
+ // Check localhost/loopback before iterating suffixes.
337
+ // "localhost" is an implicit default gateway (defaultKnownGateways)
338
+ // not present in cfg.Gateway.PublicGateways, so it won't appear
339
+ // in suffixes.
340
+ if host == "localhost" || strings.HasSuffix(host, ".localhost") {
341
+ return localhostAttr
342
+ }
343
+ if strings.HasPrefix(host, "127.") || host == "::1" {
344
+ return loopbackAttr
345
+ }
346
+
347
+ for i, suffix := range suffixes {
348
+ if strings.HasSuffix(host, suffix) {
349
+ return suffixAttrs[i]
350
+ }
351
+ }
352
+
353
+ return otherAttr
354
+ }
355
+}
356
+
357
var subdomainGatewaySpec = &gateway.PublicGateway{
358
Paths: defaultPaths,
359
UseSubdomains: true,