diff --git a/examples/gateway/car/main.go b/examples/gateway/car/main.go index 070dacb94..b0884e1f2 100644 --- a/examples/gateway/car/main.go +++ b/examples/gateway/car/main.go @@ -14,6 +14,8 @@ import ( "github.com/ipfs/go-libipfs/examples/gateway/common" "github.com/ipfs/go-libipfs/gateway" carblockstore "github.com/ipld/go-car/v2/blockstore" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { @@ -52,9 +54,23 @@ func main() { UseSubdomains: true, }, } - handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink) + + // Creates a mux to serve the prometheus metrics alongside the gateway. This + // step is optional and only required if you need or want to access the metrics. + // You may also decide to expose the metrics on a different path, or port. + mux := http.NewServeMux() + mux.Handle("/debug/metrics/prometheus", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})) + mux.Handle("/", handler) + + // Then wrap the mux with the hostname handler. Please note that the metrics + // will not be available under the previously defined publicGateways. + // You will be able to access the metrics via 127.0.0.1 but not localhost + // or example.net. If you want to expose the metrics on such gateways, + // you will have to add the path "/debug" to the variable Paths. + handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink) log.Printf("Listening on http://localhost:%d", *port) + log.Printf("Metrics available at http://127.0.0.1:%d/debug/metrics/prometheus", *port) for _, cid := range roots { log.Printf("Hosting CAR root at http://localhost:%d/ipfs/%s", *port, cid.String()) } diff --git a/examples/gateway/proxy/main.go b/examples/gateway/proxy/main.go index 6f9282c19..793fee121 100644 --- a/examples/gateway/proxy/main.go +++ b/examples/gateway/proxy/main.go @@ -10,6 +10,8 @@ import ( offline "github.com/ipfs/go-ipfs-exchange-offline" "github.com/ipfs/go-libipfs/examples/gateway/common" "github.com/ipfs/go-libipfs/gateway" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { @@ -50,9 +52,25 @@ func main() { UseSubdomains: true, }, } - handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink) + + // Creates a mux to serve the prometheus metrics alongside the gateway. This + // step is optional and only required if you need or want to access the metrics. + // You may also decide to expose the metrics on a different path, or port. + mux := http.NewServeMux() + mux.Handle("/debug/metrics/prometheus", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})) + mux.Handle("/", handler) + + // Then wrap the mux with the hostname handler. Please note that the metrics + // will not be available under the previously defined publicGateways. + // You will be able to access the metrics via 127.0.0.1 but not localhost + // or example.net. If you want to expose the metrics on such gateways, + // you will have to add the path "/debug" to the variable Paths. + handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink) log.Printf("Listening on http://localhost:%d", *port) + log.Printf("Try loading an image: http://localhost:%d/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", *port) + log.Printf("Try browsing Wikipedia snapshot: http://localhost:%d/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", *port) + log.Printf("Metrics available at http://127.0.0.1:%d/debug/metrics/prometheus", *port) if err := http.ListenAndServe(":"+strconv.Itoa(*port), handler); err != nil { log.Fatal(err) } diff --git a/examples/gateway/proxy/routing.go b/examples/gateway/proxy/routing.go index 24a6903ae..57d0c1492 100644 --- a/examples/gateway/proxy/routing.go +++ b/examples/gateway/proxy/routing.go @@ -89,7 +89,6 @@ func (ps *proxyRouting) fetch(ctx context.Context, id peer.ID) ([]byte, error) { }, }) if err != nil { - fmt.Println(err) return nil, err } defer resp.Body.Close() diff --git a/examples/go.mod b/examples/go.mod index e4f837ad2..b6d0b1372 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -22,6 +22,7 @@ require ( github.com/ipld/go-codec-dagpb v1.5.0 github.com/ipld/go-ipld-prime v0.19.0 github.com/libp2p/go-libp2p v0.24.2 + github.com/prometheus/client_golang v1.14.0 github.com/stretchr/testify v1.8.1 ) @@ -89,7 +90,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect diff --git a/gateway/handler.go b/gateway/handler.go index 50f7aabc6..a4716e2db 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -74,10 +74,14 @@ type handler struct { unixfsGetMetric *prometheus.SummaryVec // deprecated, use firstContentBlockGetMetric // response type metrics - unixfsFileGetMetric *prometheus.HistogramVec - unixfsGenDirGetMetric *prometheus.HistogramVec - carStreamGetMetric *prometheus.HistogramVec - rawBlockGetMetric *prometheus.HistogramVec + getMetric *prometheus.HistogramVec + unixfsFileGetMetric *prometheus.HistogramVec + unixfsGenDirGetMetric *prometheus.HistogramVec + carStreamGetMetric *prometheus.HistogramVec + rawBlockGetMetric *prometheus.HistogramVec + tarStreamGetMetric *prometheus.HistogramVec + jsoncborDocumentGetMetric *prometheus.HistogramVec + ipnsRecordGetMetric *prometheus.HistogramVec } // StatusResponseWriter enables us to override HTTP Status Code passed to @@ -232,6 +236,11 @@ func newHandler(c Config, api API) *handler { // Response-type specific metrics // ---------------------------- + // Generic: time it takes to execute a successful gateway request (all request types) + getMetric: newHistogramMetric( + "gw_get_duration_seconds", + "The time to GET a successful response to a request (all content types).", + ), // UnixFS: time it takes to return a file unixfsFileGetMetric: newHistogramMetric( "gw_unixfs_file_get_duration_seconds", @@ -252,13 +261,28 @@ func newHandler(c Config, api API) *handler { "gw_raw_block_get_duration_seconds", "The time to GET an entire raw Block from the gateway.", ), + // TAR: time it takes to return requested TAR stream + tarStreamGetMetric: newHistogramMetric( + "gw_tar_stream_get_duration_seconds", + "The time to GET an entire TAR stream from the gateway.", + ), + // JSON/CBOR: time it takes to return requested DAG-JSON/-CBOR document + jsoncborDocumentGetMetric: newHistogramMetric( + "gw_jsoncbor_get_duration_seconds", + "The time to GET an entire DAG-JSON/CBOR block from the gateway.", + ), + // IPNS Record: time it takes to return IPNS record + ipnsRecordGetMetric: newHistogramMetric( + "gw_ipns_record_get_duration_seconds", + "The time to GET an entire IPNS Record from the gateway.", + ), // Legacy Metrics // ---------------------------- unixfsGetMetric: newSummaryMetric( // TODO: remove? // (deprecated, use firstContentBlockGetMetric instead) "unixfs_get_latency_seconds", - "The time to receive the first UnixFS node on a GET from the gateway.", + "DEPRECATED: does not do what you think, use gw_first_content_block_get_latency_seconds instead.", ), } return i @@ -372,43 +396,44 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } + var success bool + // Support custom response formats passed via ?format or Accept HTTP header switch responseFormat { case "", "application/json", "application/cbor": switch mc.Code(resolvedPath.Cid().Prefix().Codec) { case mc.Json, mc.DagJson, mc.Cbor, mc.DagCbor: logger.Debugw("serving codec", "path", contentPath) - i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) + success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) default: logger.Debugw("serving unixfs", "path", contentPath) - i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger) + success = i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger) } - return case "application/vnd.ipld.raw": logger.Debugw("serving raw block", "path", contentPath) - i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin) - return + success = i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin) case "application/vnd.ipld.car": logger.Debugw("serving car stream", "path", contentPath) carVersion := formatParams["version"] - i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin) - return + success = i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin) case "application/x-tar": logger.Debugw("serving tar file", "path", contentPath) - i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - return + success = i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger) case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor": logger.Debugw("serving codec", "path", contentPath) - i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) + success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) case "application/vnd.ipfs.ipns-record": logger.Debugw("serving ipns record", "path", contentPath) - i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - return + success = i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger) default: // catch-all for unsuported application/vnd.* err := fmt.Errorf("unsupported format %q", responseFormat) webError(w, "failed to respond with requested content type", err, http.StatusBadRequest) return } + + if success { + i.getMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + } } func (i *handler) addUserHeaders(w http.ResponseWriter) { diff --git a/gateway/handler_block.go b/gateway/handler_block.go index d9e1b137b..fcb2408bc 100644 --- a/gateway/handler_block.go +++ b/gateway/handler_block.go @@ -12,14 +12,15 @@ import ( ) // serveRawBlock returns bytes behind a raw block -func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) { +func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) bool { ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() + blockCid := resolvedPath.Cid() block, err := i.api.GetBlock(ctx, blockCid) if err != nil { webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return + return false } content := bytes.NewReader(block.RawData()) @@ -45,4 +46,6 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h // Update metrics i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) } + + return dataSent } diff --git a/gateway/handler_car.go b/gateway/handler_car.go index b52b113ac..b900d699a 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -16,9 +16,10 @@ import ( ) // serveCAR returns a CAR stream for specific DAG+selector -func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) { +func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) bool { ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() + ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -28,7 +29,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R default: err := fmt.Errorf("only version=1 is supported") webError(w, "unsupported CAR version", err, http.StatusBadRequest) - return + return false } rootCid := resolvedPath.Cid() @@ -55,7 +56,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R // Finish early if Etag match if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) - return + return false } // Make it clear we don't support range-requests over a car stream @@ -79,11 +80,12 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R // Due to this, we suggest client always verify that // the received CAR stream response is matching requested DAG selector w.Header().Set("X-Stream-Error", err.Error()) - return + return false } // Update metrics i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + return true } // FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315 diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go index ba981cd02..9959ddffb 100644 --- a/gateway/handler_codec.go +++ b/gateway/handler_codec.go @@ -51,7 +51,7 @@ var contentTypeToExtension = map[string]string{ "application/vnd.ipld.dag-cbor": ".cbor", } -func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) { +func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) bool { ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType))) defer span.End() @@ -65,7 +65,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder()) err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path) webError(w, "unsupported pathing", err, http.StatusNotImplemented) - return + return false } // If no explicit content type was requested, the response will have one based on the codec from the CID @@ -75,7 +75,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http // Should not happen unless function is called with wrong parameters. err := fmt.Errorf("content type not found for codec: %v", cidCodec) webError(w, "internal error", err, http.StatusInternalServerError) - return + return false } responseContentType = cidContentType } @@ -94,14 +94,12 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http download := r.URL.Query().Get("download") == "true" if isDAG && acceptsHTML && !download { - i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath) + return i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath) } else { // This covers CIDs with codec 'json' and 'cbor' as those do not have // an explicit requested content type. - i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime) + return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin) } - - return } // If DAG-JSON or DAG-CBOR was requested using corresponding plain content type @@ -110,8 +108,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http if ok { for _, skipCodec := range skipCodecs { if skipCodec == cidCodec { - i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime) - return + return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin) } } } @@ -123,14 +120,14 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http // This is never supposed to happen unless function is called with wrong parameters. err := fmt.Errorf("unsupported content type: %s", requestedContentType) webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } // This handles DAG-* conversions and validations. - i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime) + return i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime, begin) } -func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) { +func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) bool { // A HTML directory index will be presented, be sure to set the correct // type instead of relying on autodetection (which may fail). w.Header().Set("Content-Type", "text/html") @@ -155,51 +152,61 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r * CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)), }); err != nil { webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError) + return false } + + return true } // serveCodecRaw returns the raw block without any conversion -func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) { +func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime, begin time.Time) bool { blockCid := resolvedPath.Cid() block, err := i.api.GetBlock(ctx, blockCid) if err != nil { webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return + return false } content := bytes.NewReader(block.RawData()) // ServeContent will take care of // If-None-Match+Etag, Content-Length and range requests - _, _, _ = ServeContent(w, r, name, modtime, content) + _, dataSent, _ := ServeContent(w, r, name, modtime, content) + + if dataSent { + // Update metrics + i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + } + + return dataSent } // serveCodecConverted returns payload converted to codec specified in toCodec -func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) { +func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime, begin time.Time) bool { blockCid := resolvedPath.Cid() block, err := i.api.GetBlock(ctx, blockCid) if err != nil { webError(w, "ipfs block get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError) - return + return false } codec := blockCid.Prefix().Codec decoder, err := multicodec.LookupDecoder(codec) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } node := basicnode.Prototype.Any.NewBuilder() err = decoder(node, bytes.NewReader(block.RawData())) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } encoder, err := multicodec.LookupEncoder(uint64(toCodec)) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } // Ensure IPLD node conforms to the codec specification. @@ -207,7 +214,7 @@ func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter err = encoder(node.Build(), &buf) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } // Sets correct Last-Modified header. This code is borrowed from the standard @@ -216,7 +223,14 @@ func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) } - _, _ = w.Write(buf.Bytes()) + _, err = w.Write(buf.Bytes()) + if err == nil { + // Update metrics + i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + return true + } + + return false } func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string { diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go index 50d99e231..e2f658579 100644 --- a/gateway/handler_ipns_record.go +++ b/gateway/handler_ipns_record.go @@ -12,14 +12,19 @@ import ( "github.com/ipfs/go-cid" ipns_pb "github.com/ipfs/go-ipns/pb" ipath "github.com/ipfs/interface-go-ipfs-core/path" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) -func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { +func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { + ctx, span := spanTrace(ctx, "ServeIPNSRecord", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) + defer span.End() + if contentPath.Namespace() != "ipns" { err := fmt.Errorf("%s is not an IPNS link", contentPath.String()) webError(w, err.Error(), err, http.StatusBadRequest) - return + return false } key := contentPath.String() @@ -28,26 +33,26 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r if strings.Count(key, "/") != 0 { err := errors.New("cannot find ipns key for subpath") webError(w, err.Error(), err, http.StatusBadRequest) - return + return false } c, err := cid.Decode(key) if err != nil { webError(w, err.Error(), err, http.StatusBadRequest) - return + return false } rawRecord, err := i.api.GetIPNSRecord(ctx, c) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } var record ipns_pb.IpnsEntry err = proto.Unmarshal(rawRecord, &record) if err != nil { webError(w, err.Error(), err, http.StatusInternalServerError) - return + return false } // Set cache control headers based on the TTL set in the IPNS record. If the @@ -74,5 +79,12 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record") w.Header().Set("X-Content-Type-Options", "nosniff") - _, _ = w.Write(rawRecord) + _, err = w.Write(rawRecord) + if err == nil { + // Update metrics + i.ipnsRecordGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + return true + } + + return false } diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go index decdf87c9..9c7026d33 100644 --- a/gateway/handler_tar.go +++ b/gateway/handler_tar.go @@ -15,7 +15,7 @@ import ( var unixEpochTime = time.Unix(0, 0) -func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { +func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() @@ -26,7 +26,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R file, err := i.api.GetUnixFsNode(ctx, resolvedPath) if err != nil { webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return + return false } defer file.Close() @@ -46,7 +46,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R // Finish early if Etag match if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) - return + return false } // Set Content-Disposition @@ -62,7 +62,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R tarw, err := files.NewTarWriter(w) if err != nil { webError(w, "could not build tar writer", err, http.StatusInternalServerError) - return + return false } defer tarw.Close() @@ -86,6 +86,10 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R // (1) detect error by having corrupted TAR // (2) be able to reason what went wrong by instecting the tail of TAR stream _, _ = w.Write([]byte(err.Error())) - return + return false } + + // Update metrics + i.tarStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) + return true } diff --git a/gateway/handler_unixfs.go b/gateway/handler_unixfs.go index c443e4b32..c8c37ea43 100644 --- a/gateway/handler_unixfs.go +++ b/gateway/handler_unixfs.go @@ -14,7 +14,7 @@ import ( "go.uber.org/zap" ) -func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { +func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() @@ -22,7 +22,7 @@ func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *htt dr, err := i.api.GetUnixFsNode(ctx, resolvedPath) if err != nil { webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return + return false } defer dr.Close() @@ -30,16 +30,17 @@ func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *htt if f, ok := dr.(files.File); ok { logger.Debugw("serving unixfs file", "path", contentPath) i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin) - return + return false } // Handling Unixfs directory dir, ok := dr.(files.Directory) if !ok { internalWebError(w, fmt.Errorf("unsupported UnixFS type")) - return + return false } logger.Debugw("serving unixfs directory", "path", contentPath) i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger) + return true }