Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): IPFSBackend metrics and HTTP range support #245

Merged
merged 4 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions blockservice/blockservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func getBlock(ctx context.Context, c cid.Cid, bs blockstore.Blockstore, fget fun

// TODO be careful checking ErrNotFound. If the underlying
// implementation changes, this will break.
logger.Debug("Blockservice: Searching bitswap")
logger.Debug("BlockService: Searching")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ there is no bitswap in bifrost-gateway, yet we got super confusing "Searching bitswap" message here ;)

blk, err := f.GetBlock(ctx, c)
if err != nil {
return nil, err
Expand All @@ -262,7 +262,7 @@ func getBlock(ctx context.Context, c cid.Cid, bs blockstore.Blockstore, fget fun
return blk, nil
}

logger.Debug("Blockservice GetBlock: Not found")
logger.Debug("BlockService GetBlock: Not found")
return nil, err
}

Expand Down
21 changes: 1 addition & 20 deletions gateway/blocks_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func NewBlocksGateway(blockService blockservice.BlockService, opts ...BlockGatew
}, nil
}

func (api *BlocksGateway) Get(ctx context.Context, path ImmutablePath) (ContentPathMetadata, *GetResponse, error) {
func (api *BlocksGateway) Get(ctx context.Context, path ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) {
md, nd, err := api.getNode(ctx, path)
if err != nil {
return md, nil, err
Expand Down Expand Up @@ -180,25 +180,6 @@ func (api *BlocksGateway) Get(ctx context.Context, path ImmutablePath) (ContentP
return ContentPathMetadata{}, nil, fmt.Errorf("data was not a valid file or directory: %w", ErrInternalServerError) // TODO: should there be a gateway invalid content type to abstract over the various IPLD error types?
}

func (api *BlocksGateway) GetRange(ctx context.Context, path ImmutablePath, ranges ...GetRange) (ContentPathMetadata, files.File, error) {
md, nd, err := api.getNode(ctx, path)
if err != nil {
return md, nil, err
}

// This code path covers full graph, single file/directory, and range requests
n, err := ufile.NewUnixfsFile(ctx, api.dagService, nd)
if err != nil {
return md, nil, err
}
f, ok := n.(files.File)
if !ok {
return ContentPathMetadata{}, nil, NewErrorResponse(fmt.Errorf("can only do range requests on files, but did not get a file"), http.StatusBadRequest)
}

return md, f, nil
}

func (api *BlocksGateway) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) {
md, nd, err := api.getNode(ctx, path)
if err != nil {
Expand Down
33 changes: 22 additions & 11 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ type ContentPathMetadata struct {
ContentType string // Only used for UnixFS requests
}

// GetRange describes a range request within a UnixFS file. From and To mostly follow HTTP Range Request semantics.
// ByteRange describes a range request within a UnixFS file. From and To mostly follow [HTTP Byte Range] Request semantics.
// From >= 0 and To = nil: Get the file (From, Length)
// From >= 0 and To >= 0: Get the range (From, To)
// From >= 0 and To <0: Get the range (From, Length - To)
type GetRange struct {
//
// [HTTP Byte Range]: https://httpwg.org/specs/rfc9110.html#rfc.section.14.1.2
type ByteRange struct {
From uint64
To *int64
}
Expand Down Expand Up @@ -86,15 +88,24 @@ func NewGetResponseFromDirectoryListing(dagSize uint64, entries <-chan unixfs.Li
// There are also some existing error types that the gateway code knows how to handle (e.g. context.DeadlineExceeded
// and various IPLD pathing related errors).
type IPFSBackend interface {
// Get returns a UnixFS file, UnixFS directory, or an IPLD block depending on what the path is that has been
// requested. Directories' files.DirEntry objects do not need to contain content, but must contain Name,
// Size, and Cid.
Get(context.Context, ImmutablePath) (ContentPathMetadata, *GetResponse, error)

// GetRange returns a full UnixFS file object. Ranges passed in are advisory for pre-fetching data, however
// consumers of this function may require extra data beyond the passed ranges (e.g. the initial bit of the file
// might be used for content type sniffing even if only the end of the file is requested).
GetRange(context.Context, ImmutablePath, ...GetRange) (ContentPathMetadata, files.File, error)

// Get returns a GetResponse with UnixFS file, directory or a block in IPLD
// format e.g., (DAG-)CBOR/JSON.
//
// Returned Directories are preferably a minimum info required for enumeration: Name, Size, and Cid.
//
// Optional ranges follow [HTTP Byte Ranges] notation and can be used for
// pre-fetching specific sections of a file or a block.
//
// Range notes:
// - Generating response to a range request may require additional data
// beyond the passed ranges (e.g. a single byte range from the middle of a
// file will still need magic bytes from the very beginning for content
// type sniffing).
// - A range request for a directory currently holds no semantic meaning.
//
// [HTTP Byte Ranges]: https://httpwg.org/specs/rfc9110.html#rfc.section.14.1.2
Get(context.Context, ImmutablePath, ...ByteRange) (ContentPathMetadata, *GetResponse, error)
Copy link
Member Author

@lidel lidel Apr 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ removed GetRange, we now have Get with optional range requests


// GetAll returns a UnixFS file or directory depending on what the path is that has been requested. Directories should
// include all content recursively.
Expand Down
8 changes: 2 additions & 6 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,8 @@ func newMockAPI(t *testing.T) (*mockAPI, cid.Cid) {
}, cids[0]
}

func (api *mockAPI) Get(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, *GetResponse, error) {
return api.gw.Get(ctx, immutablePath)
}

func (api *mockAPI) GetRange(ctx context.Context, immutablePath ImmutablePath, ranges ...GetRange) (ContentPathMetadata, files.File, error) {
return api.gw.GetRange(ctx, immutablePath, ranges...)
func (api *mockAPI) Get(ctx context.Context, immutablePath ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) {
return api.gw.Get(ctx, immutablePath, ranges...)
}

func (api *mockAPI) GetAll(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) {
Expand Down
134 changes: 6 additions & 128 deletions gateway/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
cid "github.com/ipfs/go-cid"
logging "github.com/ipfs/go-log"
prometheus "github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
Expand Down Expand Up @@ -81,6 +80,12 @@ type handler struct {
ipnsRecordGetMetric *prometheus.HistogramVec
}

// NewHandler returns an http.Handler that can act as a gateway to IPFS content
// offlineApi is a version of the API that should not make network requests for missing data
func NewHandler(c Config, api IPFSBackend) http.Handler {
return newHandlerWithMetrics(c, api)
}

// StatusResponseWriter enables us to override HTTP Status Code passed to
// WriteHeader function inside of http.ServeContent. Decision is based on
// presence of HTTP Headers such as Location.
Expand Down Expand Up @@ -149,128 +154,6 @@ func (w *errRecordingResponseWriter) ReadFrom(r io.Reader) (n int64, err error)
return n, err
}

func newSummaryMetric(name string, help string) *prometheus.SummaryVec {
summaryMetric := prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: "ipfs",
Subsystem: "http",
Name: name,
Help: help,
},
[]string{"gateway"},
)
if err := prometheus.Register(summaryMetric); err != nil {
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
summaryMetric = are.ExistingCollector.(*prometheus.SummaryVec)
} else {
log.Errorf("failed to register ipfs_http_%s: %v", name, err)
}
}
return summaryMetric
}

func newHistogramMetric(name string, help string) *prometheus.HistogramVec {
// We can add buckets as a parameter in the future, but for now using static defaults
// suggested in https://github.com/ipfs/kubo/issues/8441
defaultBuckets := []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60}
histogramMetric := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "ipfs",
Subsystem: "http",
Name: name,
Help: help,
Buckets: defaultBuckets,
},
[]string{"gateway"},
)
if err := prometheus.Register(histogramMetric); err != nil {
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
histogramMetric = are.ExistingCollector.(*prometheus.HistogramVec)
} else {
log.Errorf("failed to register ipfs_http_%s: %v", name, err)
}
}
return histogramMetric
}

// NewHandler returns an http.Handler that can act as a gateway to IPFS content
// offlineApi is a version of the API that should not make network requests for missing data
func NewHandler(c Config, api IPFSBackend) http.Handler {
return newHandler(c, api)
}

func newHandler(c Config, api IPFSBackend) *handler {
i := &handler{
config: c,
api: api,
// Improved Metrics
// ----------------------------
// Time till the first content block (bar in /ipfs/cid/foo/bar)
// (format-agnostic, across all response types)
firstContentBlockGetMetric: newHistogramMetric(
"gw_first_content_block_get_latency_seconds",
"The time till the first content block is received on GET from the gateway.",
),

// 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",
"The time to serve an entire UnixFS file from the gateway.",
),
// UnixFS: time it takes to find and serve an index.html file on behalf of a directory.
unixfsDirIndexGetMetric: newHistogramMetric(
"gw_unixfs_dir_indexhtml_get_duration_seconds",
"The time to serve an index.html file on behalf of a directory from the gateway. This is a subset of gw_unixfs_file_get_duration_seconds.",
),
// UnixFS: time it takes to generate static HTML with directory listing
unixfsGenDirListingGetMetric: newHistogramMetric(
"gw_unixfs_gen_dir_listing_get_duration_seconds",
"The time to serve a generated UnixFS HTML directory listing from the gateway.",
),
// CAR: time it takes to return requested CAR stream
carStreamGetMetric: newHistogramMetric(
"gw_car_stream_get_duration_seconds",
"The time to GET an entire CAR stream from the gateway.",
),
// Block: time it takes to return requested Block
rawBlockGetMetric: newHistogramMetric(
"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",
"DEPRECATED: does not do what you think, use gw_first_content_block_get_latency_seconds instead.",
),
}
return i
}

func (i *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer panicHandler(w)

Expand Down Expand Up @@ -887,8 +770,3 @@ func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentP

return true
}

// spanTrace starts a new span using the standard IPFS tracing conventions.
func spanTrace(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return otel.Tracer("boxo").Start(ctx, fmt.Sprintf("%s.%s", " Gateway", spanName), opts...)
}
Loading