diff --git a/pkg/feeds/factory/factory.go b/pkg/feeds/factory/factory.go index 1d555416407..44b7e911a2a 100644 --- a/pkg/feeds/factory/factory.go +++ b/pkg/feeds/factory/factory.go @@ -5,27 +5,88 @@ package factory import ( + "context" + "errors" + "time" + "github.com/ethersphere/bee/v2/pkg/feeds" "github.com/ethersphere/bee/v2/pkg/feeds/epochs" "github.com/ethersphere/bee/v2/pkg/feeds/sequence" + m "github.com/ethersphere/bee/v2/pkg/metrics" storage "github.com/ethersphere/bee/v2/pkg/storage" + "github.com/ethersphere/bee/v2/pkg/swarm" ) +var _ m.Collector = (*factory)(nil) + type factory struct { storage.Getter + metrics metrics } func New(getter storage.Getter) feeds.Factory { - return &factory{getter} + return &factory{ + Getter: getter, + metrics: newMetrics(), + } } func (f *factory) NewLookup(t feeds.Type, feed *feeds.Feed) (feeds.Lookup, error) { + var lookup feeds.Lookup switch t { case feeds.Sequence: - return sequence.NewAsyncFinder(f.Getter, feed), nil + lookup = sequence.NewAsyncFinder(f.Getter, feed) case feeds.Epoch: - return epochs.NewAsyncFinder(f.Getter, feed), nil + lookup = epochs.NewAsyncFinder(f.Getter, feed) + default: + return nil, feeds.ErrFeedTypeNotFound + } + + return f.wrapLookup(t, lookup), nil +} + +func (f *factory) wrapLookup(t feeds.Type, lookup feeds.Lookup) feeds.Lookup { + return &instrumentedLookup{ + lookup: lookup, + typ: lookupType(t), + m: f.metrics, } +} + +type instrumentedLookup struct { + lookup feeds.Lookup + typ string + m metrics +} - return nil, feeds.ErrFeedTypeNotFound +func (l *instrumentedLookup) At(ctx context.Context, at int64, after uint64) (swarm.Chunk, feeds.Index, feeds.Index, error) { + l.m.LookupStarted.WithLabelValues(l.typ).Inc() + start := time.Now() + ch, cur, next, err := l.lookup.At(ctx, at, after) + l.m.LookupDuration.WithLabelValues(l.typ, lookupResult(ch, err)).Observe(time.Since(start).Seconds()) + return ch, cur, next, err +} + +func lookupType(t feeds.Type) string { + switch t { + case feeds.Sequence: + return "sequence" + case feeds.Epoch: + return "epoch" + default: + return "unknown" + } +} + +func lookupResult(ch swarm.Chunk, err error) string { + switch { + case err != nil && errors.Is(err, context.Canceled): + return "canceled" + case err != nil: + return "error" + case ch == nil: + return "not_found" + default: + return "found" + } } diff --git a/pkg/feeds/factory/metrics.go b/pkg/feeds/factory/metrics.go new file mode 100644 index 00000000000..95888a1414a --- /dev/null +++ b/pkg/feeds/factory/metrics.go @@ -0,0 +1,49 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package factory + +import ( + "github.com/prometheus/client_golang/prometheus" + + m "github.com/ethersphere/bee/v2/pkg/metrics" +) + +type metrics struct { + // all metrics fields must be exported + // to be able to return them by Metrics() + // using reflection + LookupStarted *prometheus.CounterVec + LookupDuration *prometheus.HistogramVec +} + +func newMetrics() metrics { + subsystem := "feeds" + + return metrics{ + LookupStarted: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "lookup_started_total", + Help: "Number of feed lookup attempts started.", + }, + []string{"type"}, + ), + LookupDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "lookup_duration_seconds", + Help: "Histogram of feed lookup durations.", + Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 30, 60}, + }, + []string{"type", "result"}, + ), + } +} + +func (f *factory) Metrics() []prometheus.Collector { + return m.PrometheusCollectorsFromFields(f.metrics) +} diff --git a/pkg/node/node.go b/pkg/node/node.go index 9ef7704b9dd..3c4a45f6b3a 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -1414,6 +1414,7 @@ func NewBee( apiService.MustRegisterMetrics(retrieval.Metrics()...) apiService.MustRegisterMetrics(lightNodes.Metrics()...) apiService.MustRegisterMetrics(hive.Metrics()...) + apiService.MustRegisterMetrics(feedFactory.(metrics.Collector).Metrics()...) if bs, ok := batchStore.(metrics.Collector); ok { apiService.MustRegisterMetrics(bs.Metrics()...)