Repository: jaegertracing/jaeger

Issue:
[Bug]: There are no metrics from jaeger_storage exporter
### What happened?

When running jaeger-v2 I want to see metrics from the storage exporter, like counts of batches & spans, export latency, error counts.

### Steps to reproduce

Run jaeger-v2, send some data, curl the metrics endpoint

### Expected behavior

This comment https://github.com/jaegertracing/jaeger/pull/7157#discussion_r2121544811 shows existing metrics. They include metrics like `otelcol_processor_***`, but nothing for the exporter.

### Relevant log output

```shell

```

### Screenshot

_No response_

### Additional context

_No response_

### Jaeger backend version

_No response_

### SDK

_No response_

### Pipeline

_No response_

### Stogage backend

_No response_

### Operating system

_No response_

### Deployment model

_No response_

### Deployment configs

```shell

```

Hints from issue comments:

Solution patch:
diff --git a/cmd/jaeger/internal/extension/jaegerstorage/extension.go b/cmd/jaeger/internal/extension/jaegerstorage/extension.go
index 539a814c..0084d018 100644
--- a/cmd/jaeger/internal/extension/jaegerstorage/extension.go
+++ b/cmd/jaeger/internal/extension/jaegerstorage/extension.go
@@ -227,6 +227,7 @@ func (s *storageExt) Start(ctx context.Context, host component.Host) error {
 			esTelset := telset
 			esTelset.Metrics = scopedMetricsFactory(metricStorageName, "elasticsearch", "metricstore")
 			metricStoreFactory, err = esmetrics.NewFactory(
+				ctx,
 				*cfg.Elasticsearch,
 				esTelset,
 			)
@@ -250,6 +251,13 @@ func (s *storageExt) Shutdown(context.Context) error {
 			}
 		}
 	}
+	for _, metricfactory := range s.metricsFactories {
+		if closer, ok := metricfactory.(io.Closer); ok {
+			if err := closer.Close(); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
 	return errors.Join(errs...)
 }
 
diff --git a/internal/storage/elasticsearch/config/config.go b/internal/storage/elasticsearch/config/config.go
index d006aba3..14789feb 100644
--- a/internal/storage/elasticsearch/config/config.go
+++ b/internal/storage/elasticsearch/config/config.go
@@ -221,11 +221,11 @@ type BearerTokenAuthentication struct {
 }
 
 // NewClient creates a new ElasticSearch client
-func NewClient(c *Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error) {
+func NewClient(ctx context.Context, c *Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error) {
 	if len(c.Servers) < 1 {
 		return nil, errors.New("no servers specified")
 	}
-	options, err := c.getConfigOptions(logger)
+	options, err := c.getConfigOptions(ctx, logger)
 	if err != nil {
 		return nil, err
 	}
@@ -249,14 +249,14 @@ func NewClient(c *Configuration, logger *zap.Logger, metricsFactory metrics.Fact
 		Workers(c.BulkProcessing.Workers).
 		BulkActions(c.BulkProcessing.MaxActions).
 		FlushInterval(c.BulkProcessing.FlushInterval).
-		Do(context.Background())
+		Do(ctx)
 	if err != nil {
 		return nil, err
 	}
 
 	if c.Version == 0 {
 		// Determine ElasticSearch Version
-		pingResult, _, err := rawClient.Ping(c.Servers[0]).Do(context.Background())
+		pingResult, _, err := rawClient.Ping(c.Servers[0]).Do(ctx)
 		if err != nil {
 			return nil, err
 		}
@@ -282,7 +282,7 @@ func NewClient(c *Configuration, logger *zap.Logger, metricsFactory metrics.Fact
 
 	var rawClientV8 *esV8.Client
 	if c.Version >= 8 {
-		rawClientV8, err = newElasticsearchV8(c, logger)
+		rawClientV8, err = newElasticsearchV8(ctx, c, logger)
 		if err != nil {
 			return nil, fmt.Errorf("error creating v8 client: %w", err)
 		}
@@ -337,14 +337,14 @@ func (bcb *bulkCallback) invoke(id int64, requests []elastic.BulkableRequest, re
 	}
 }
 
-func newElasticsearchV8(c *Configuration, logger *zap.Logger) (*esV8.Client, error) {
+func newElasticsearchV8(ctx context.Context, c *Configuration, logger *zap.Logger) (*esV8.Client, error) {
 	var options esV8.Config
 	options.Addresses = c.Servers
 	options.Username = c.Authentication.BasicAuthentication.Username
 	options.Password = c.Authentication.BasicAuthentication.Password
 	options.DiscoverNodesOnStart = c.Sniffing.Enabled
 	options.CompressRequestBody = c.HTTPCompression
-	transport, err := GetHTTPRoundTripper(c, logger)
+	transport, err := GetHTTPRoundTripper(ctx, c, logger)
 	if err != nil {
 		return nil, err
 	}
@@ -483,7 +483,7 @@ func (c *Configuration) TagKeysAsFields() ([]string, error) {
 }
 
 // getConfigOptions wraps the configs to feed to the ElasticSearch client init
-func (c *Configuration) getConfigOptions(logger *zap.Logger) ([]elastic.ClientOptionFunc, error) {
+func (c *Configuration) getConfigOptions(ctx context.Context, logger *zap.Logger) ([]elastic.ClientOptionFunc, error) {
 	// Disable health check when token from context is allowed, this is because at this time
 	// we don'r have a valid token to do the check ad if we don't disable the check the service that
 	// uses this won't start.
@@ -523,7 +523,7 @@ func (c *Configuration) getConfigOptions(logger *zap.Logger) ([]elastic.ClientOp
 		return options, err
 	}
 
-	transport, err := GetHTTPRoundTripper(c, logger)
+	transport, err := GetHTTPRoundTripper(ctx, c, logger)
 	if err != nil {
 		return nil, err
 	}
@@ -565,9 +565,9 @@ func addLoggerOptions(options []elastic.ClientOptionFunc, logLevel string, logge
 }
 
 // GetHTTPRoundTripper returns configured http.RoundTripper
-func GetHTTPRoundTripper(c *Configuration, logger *zap.Logger) (http.RoundTripper, error) {
+func GetHTTPRoundTripper(ctx context.Context, c *Configuration, logger *zap.Logger) (http.RoundTripper, error) {
 	if !c.TLS.Insecure {
-		ctlsConfig, err := c.TLS.LoadTLSConfig(context.Background())
+		ctlsConfig, err := c.TLS.LoadTLSConfig(ctx)
 		if err != nil {
 			return nil, err
 		}
@@ -583,7 +583,7 @@ func GetHTTPRoundTripper(c *Configuration, logger *zap.Logger) (http.RoundTrippe
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: c.TLS.InsecureSkipVerify},
 	}
 	if c.TLS.CAFile != "" {
-		ctlsConfig, err := c.TLS.LoadTLSConfig(context.Background())
+		ctlsConfig, err := c.TLS.LoadTLSConfig(ctx)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/storage/metricstore/elasticsearch/factory.go b/internal/storage/metricstore/elasticsearch/factory.go
index 591e5109..5fef4e02 100644
--- a/internal/storage/metricstore/elasticsearch/factory.go
+++ b/internal/storage/metricstore/elasticsearch/factory.go
@@ -4,6 +4,9 @@
 package elasticsearch
 
 import (
+	"context"
+
+	es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch"
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore/metricstoremetrics"
@@ -13,25 +16,33 @@ import (
 type Factory struct {
 	config config.Configuration
 	telset telemetry.Settings
+	client es.Client
 }
 
 // NewFactory creates a new Factory with the given configuration and telemetry settings.
-func NewFactory(cfg config.Configuration, telset telemetry.Settings) (*Factory, error) {
+func NewFactory(ctx context.Context, cfg config.Configuration, telset telemetry.Settings) (*Factory, error) {
 	if err := cfg.Validate(); err != nil {
 		return nil, err
 	}
+
+	client, err := config.NewClient(ctx, &cfg, telset.Logger, telset.Metrics)
+	if err != nil {
+		return nil, err
+	}
+
 	return &Factory{
 		config: cfg,
 		telset: telset,
+		client: client,
 	}, nil
 }
 
 // CreateMetricsReader implements storage.MetricStoreFactory.
 func (f *Factory) CreateMetricsReader() (metricstore.Reader, error) {
-	mr, _ := NewMetricsReader(f.config, f.telset.Logger, f.telset.TracerProvider)
-	// Currently, the NewMetricsReader function does not return an error.
-	// if err != nil {
-	//	 return nil, err
-	// }
+	mr := NewMetricsReader(f.client, f.telset.Logger, f.telset.TracerProvider)
 	return metricstoremetrics.NewReaderDecorator(mr, f.telset.Metrics), nil
 }
+
+func (f *Factory) Close() error {
+	return f.client.Close()
+}
diff --git a/internal/storage/metricstore/elasticsearch/reader.go b/internal/storage/metricstore/elasticsearch/reader.go
index eb421c18..2dbbe14b 100644
--- a/internal/storage/metricstore/elasticsearch/reader.go
+++ b/internal/storage/metricstore/elasticsearch/reader.go
@@ -12,7 +12,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics"
-	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
+	es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore"
 )
 
@@ -22,15 +22,17 @@ const minStep = time.Millisecond
 
 // MetricsReader is a Elasticsearch metrics reader.
 type MetricsReader struct {
+	client es.Client
 	logger *zap.Logger
 	tracer trace.Tracer
 }
 
-func NewMetricsReader(_ config.Configuration, logger *zap.Logger, tracer trace.TracerProvider) (*MetricsReader, error) {
+func NewMetricsReader(client es.Client, logger *zap.Logger, tracer trace.TracerProvider) *MetricsReader {
 	return &MetricsReader{
+		client: client,
 		logger: logger,
 		tracer: tracer.Tracer("elasticsearch-metricstore"),
-	}, nil
+	}
 }
 
 func (MetricsReader) GetLatencies(_ context.Context, _ *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error) {
diff --git a/internal/storage/v1/elasticsearch/factory.go b/internal/storage/v1/elasticsearch/factory.go
index ba474e3a..b99f208c 100644
--- a/internal/storage/v1/elasticsearch/factory.go
+++ b/internal/storage/v1/elasticsearch/factory.go
@@ -42,7 +42,7 @@ type FactoryBase struct {
 	logger         *zap.Logger
 	tracer         trace.TracerProvider
 
-	newClientFn func(c *config.Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error)
+	newClientFn func(ctx context.Context, c *config.Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error)
 
 	config *config.Configuration
 
@@ -75,7 +75,7 @@ func NewFactoryBase(
 	}
 	f.tags = tags
 
-	client, err := f.newClientFn(f.config, logger, metricsFactory)
+	client, err := f.newClientFn(ctx, f.config, logger, metricsFactory)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create Elasticsearch client: %w", err)
 	}
@@ -213,7 +213,7 @@ func (f *FactoryBase) onClientPasswordChange(cfg *config.Configuration, client *
 	newCfg.Authentication.BasicAuthentication.Password = newPassword
 	newCfg.Authentication.BasicAuthentication.PasswordFilePath = "" // avoid error that both are set
 
-	newClient, err := f.newClientFn(&newCfg, f.logger, mf)
+	newClient, err := f.newClientFn(context.Background(), &newCfg, f.logger, mf)
 	if err != nil {
 		f.logger.Error("failed to recreate Elasticsearch client with new password", zap.Error(err))
 		return
diff --git a/internal/storage/v1/elasticsearch/helper.go b/internal/storage/v1/elasticsearch/helper.go
index f8e88553..b512f129 100644
--- a/internal/storage/v1/elasticsearch/helper.go
+++ b/internal/storage/v1/elasticsearch/helper.go
@@ -21,7 +21,7 @@ type mockClientBuilder struct {
 	createTemplateError error
 }
 
-func (m *mockClientBuilder) NewClient(*escfg.Configuration, *zap.Logger, metrics.Factory) (es.Client, error) {
+func (m *mockClientBuilder) NewClient(context.Context, *escfg.Configuration, *zap.Logger, metrics.Factory) (es.Client, error) {
 	if m.err == nil {
 		c := &mocks.Client{}
 		tService := &mocks.TemplateCreateService{}
@@ -48,7 +48,7 @@ func SetFactoryForTestWithCreateTemplateErr(f *FactoryBase, logger *zap.Logger,
 	f.metricsFactory = metricsFactory
 	f.config = cfg
 	f.tracer = otel.GetTracerProvider()
-	client, err := f.newClientFn(cfg, logger, metricsFactory)
+	client, err := f.newClientFn(context.Background(), cfg, logger, metricsFactory)
 	if err != nil {
 		return err
 	}
diff --git a/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics.go b/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics.go
new file mode 100644
index 00000000..cfb5754d
--- /dev/null
+++ b/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics.go
@@ -0,0 +1,144 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestoremetrics
+
+import (
+	"context"
+	"iter"
+	"time"
+
+	"go.opentelemetry.io/collector/pdata/ptrace"
+
+	"github.com/jaegertracing/jaeger/internal/metrics"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore"
+)
+
+var _ tracestore.Reader = (*ReadMetricsDecorator)(nil)
+
+// ReadMetricsDecorator wraps a tracestore.Reader and collects metrics around each read operation.
+type ReadMetricsDecorator struct {
+	traceReader          tracestore.Reader
+	findTracesMetrics    *queryMetrics
+	findTraceIDsMetrics  *queryMetrics
+	getTraceMetrics      *queryMetrics
+	getServicesMetrics   *queryMetrics
+	getOperationsMetrics *queryMetrics
+}
+
+type queryMetrics struct {
+	Errors     metrics.Counter `metric:"requests" tags:"result=err"`
+	Successes  metrics.Counter `metric:"requests" tags:"result=ok"`
+	Responses  metrics.Counter `metric:"responses"`
+	ErrLatency metrics.Timer   `metric:"latency" tags:"result=err"`
+	OKLatency  metrics.Timer   `metric:"latency" tags:"result=ok"`
+}
+
+func (q *queryMetrics) emit(err error, latency time.Duration, responses int) {
+	if err != nil {
+		q.Errors.Inc(1)
+		q.ErrLatency.Record(latency)
+	} else {
+		q.Successes.Inc(1)
+		q.OKLatency.Record(latency)
+		q.Responses.Inc(int64(responses))
+	}
+}
+
+// NewReaderDecorator returns a new ReadMetricsDecorator.
+func NewReaderDecorator(traceReader tracestore.Reader, metricsFactory metrics.Factory) *ReadMetricsDecorator {
+	return &ReadMetricsDecorator{
+		traceReader:          traceReader,
+		findTracesMetrics:    buildQueryMetrics("find_traces", metricsFactory),
+		findTraceIDsMetrics:  buildQueryMetrics("find_trace_ids", metricsFactory),
+		getTraceMetrics:      buildQueryMetrics("get_trace", metricsFactory),
+		getServicesMetrics:   buildQueryMetrics("get_services", metricsFactory),
+		getOperationsMetrics: buildQueryMetrics("get_operations", metricsFactory),
+	}
+}
+
+func buildQueryMetrics(operation string, metricsFactory metrics.Factory) *queryMetrics {
+	qMetrics := &queryMetrics{}
+	scoped := metricsFactory.Namespace(metrics.NSOptions{Name: "", Tags: map[string]string{"operation": operation}})
+	metrics.Init(qMetrics, scoped, nil)
+	return qMetrics
+}
+
+// FindTraces implements tracestore.Reader#FindTraces
+func (m *ReadMetricsDecorator) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] {
+	return func(yield func([]ptrace.Traces, error) bool) {
+		start := time.Now()
+		var err error
+		length := 0
+		defer func() {
+			m.findTracesMetrics.emit(err, time.Since(start), length)
+		}()
+		findTracesIter := m.traceReader.FindTraces(ctx, query)
+		for traces, iterErr := range findTracesIter {
+			err = iterErr
+			length += len(traces)
+			if !yield(traces, iterErr) {
+				return
+			}
+		}
+	}
+}
+
+// FindTraceIDs implements tracestore.Reader#FindTraceIDs
+func (m *ReadMetricsDecorator) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] {
+	return func(yield func([]tracestore.FoundTraceID, error) bool) {
+		start := time.Now()
+		var err error
+		length := 0
+		defer func() {
+			m.findTraceIDsMetrics.emit(err, time.Since(start), length)
+		}()
+		findTraceIDsIter := m.traceReader.FindTraceIDs(ctx, query)
+		for traceIds, iterErr := range findTraceIDsIter {
+			err = iterErr
+			length += len(traceIds)
+			if !yield(traceIds, iterErr) {
+				return
+			}
+		}
+	}
+}
+
+// GetTraces implements tracestore.Reader#GetTraces
+func (m *ReadMetricsDecorator) GetTraces(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] {
+	return func(yield func([]ptrace.Traces, error) bool) {
+		start := time.Now()
+		var err error
+		length := 0
+		defer func() {
+			m.getTraceMetrics.emit(err, time.Since(start), length)
+		}()
+		getTraceIter := m.traceReader.GetTraces(ctx, traceIDs...)
+		for traces, iterErr := range getTraceIter {
+			err = iterErr
+			length += len(traces)
+			if !yield(traces, iterErr) {
+				return
+			}
+		}
+	}
+}
+
+// GetServices implements tracestore.Reader#GetServices
+func (m *ReadMetricsDecorator) GetServices(ctx context.Context) ([]string, error) {
+	start := time.Now()
+	retMe, err := m.traceReader.GetServices(ctx)
+	m.getServicesMetrics.emit(err, time.Since(start), len(retMe))
+	return retMe, err
+}
+
+// GetOperations implements tracestore.Reader#GetOperations
+func (m *ReadMetricsDecorator) GetOperations(
+	ctx context.Context,
+	query tracestore.OperationQueryParams,
+) ([]tracestore.Operation, error) {
+	start := time.Now()
+	retMe, err := m.traceReader.GetOperations(ctx, query)
+	m.getOperationsMetrics.emit(err, time.Since(start), len(retMe))
+	return retMe, err
+}
diff --git a/internal/storage/v2/elasticsearch/factory.go b/internal/storage/v2/elasticsearch/factory.go
index f5daecc2..fb1c8de0 100644
--- a/internal/storage/v2/elasticsearch/factory.go
+++ b/internal/storage/v2/elasticsearch/factory.go
@@ -7,10 +7,12 @@ import (
 	"context"
 	"io"
 
+	"github.com/jaegertracing/jaeger/internal/metrics"
 	escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch"
 	"github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore"
 	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/tracestoremetrics"
 	v2depstore "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore"
 	v2tracestore "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/tracestore"
 	"github.com/jaegertracing/jaeger/internal/telemetry"
@@ -23,8 +25,9 @@ var (
 )
 
 type Factory struct {
-	coreFactory *elasticsearch.FactoryBase
-	config      escfg.Configuration
+	coreFactory    *elasticsearch.FactoryBase
+	config         escfg.Configuration
+	metricsFactory metrics.Factory
 }
 
 func NewFactory(ctx context.Context, cfg escfg.Configuration, telset telemetry.Settings) (*Factory, error) {
@@ -33,15 +36,16 @@ func NewFactory(ctx context.Context, cfg escfg.Configuration, telset telemetry.S
 		return nil, err
 	}
 	f := &Factory{
-		coreFactory: coreFactory,
-		config:      cfg,
+		coreFactory:    coreFactory,
+		config:         cfg,
+		metricsFactory: telset.Metrics,
 	}
 	return f, nil
 }
 
 func (f *Factory) CreateTraceReader() (tracestore.Reader, error) {
 	params := f.coreFactory.GetSpanReaderParams()
-	return v2tracestore.NewTraceReader(params), nil
+	return tracestoremetrics.NewReaderDecorator(v2tracestore.NewTraceReader(params), f.metricsFactory), nil
 }
 
 func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) {



Test patch:
diff --git a/cmd/jaeger/internal/extension/jaegerstorage/extension_test.go b/cmd/jaeger/internal/extension/jaegerstorage/extension_test.go
index 6cd689b5..3c41c80b 100644
--- a/cmd/jaeger/internal/extension/jaegerstorage/extension_test.go
+++ b/cmd/jaeger/internal/extension/jaegerstorage/extension_test.go
@@ -23,6 +23,8 @@ import (
 
 	promCfg "github.com/jaegertracing/jaeger/internal/config/promcfg"
 	esCfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
+	"github.com/jaegertracing/jaeger/internal/storage/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/badger"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/cassandra"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/memory"
@@ -42,10 +44,34 @@ func (errorFactory) CreateTraceWriter() (tracestore.Writer, error) {
 	panic("not implemented")
 }
 
+func (errorFactory) CreateMetricsReader() (metricstore.Reader, error) { panic("not implemented") }
+
 func (e errorFactory) Close() error {
 	return e.closeErr
 }
 
+func setupMockServer(t *testing.T, response []byte, statusCode int) *httptest.Server {
+	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(statusCode)
+		w.Write(response)
+	}))
+	require.NotNil(t, mockServer)
+	t.Cleanup(mockServer.Close)
+
+	return mockServer
+}
+
+func getVersionResponse(t *testing.T) []byte {
+	versionResponse, e := json.Marshal(map[string]any{
+		"Version": map[string]any{
+			"Number": "7",
+		},
+	})
+	require.NoError(t, e)
+	return versionResponse
+}
+
 func TestStorageFactoryBadHostError(t *testing.T) {
 	_, err := getStorageFactory("something", componenttest.NewNopHost())
 	require.ErrorContains(t, err, "cannot find extension")
@@ -301,11 +327,14 @@ func TestPrometheus(t *testing.T) {
 }
 
 func TestElasticsearchAsMetricsBackend(t *testing.T) {
+	server := setupMockServer(t, getVersionResponse(t), http.StatusOK)
+
 	ext := makeStorageExtension(t, &Config{
 		MetricBackends: map[string]MetricBackend{
 			"foo": {
 				Elasticsearch: &esCfg.Configuration{
-					Servers: []string{"localhost:9200"},
+					Servers:  []string{server.URL},
+					LogLevel: "info",
 				},
 			},
 		},
@@ -316,6 +345,17 @@ func TestElasticsearchAsMetricsBackend(t *testing.T) {
 	require.NoError(t, ext.Shutdown(ctx))
 }
 
+func TestMetricsBackendCloseError(t *testing.T) {
+	shutdownError := errors.New("shutdown error")
+	ext := storageExt{
+		metricsFactories: map[string]storage.MetricStoreFactory{
+			"foo": errorFactory{closeErr: shutdownError},
+		},
+	}
+	err := ext.Shutdown(context.Background())
+	require.ErrorIs(t, err, shutdownError)
+}
+
 func TestStartError(t *testing.T) {
 	ext := makeStorageExtension(t, &Config{
 		TraceBackends: map[string]TraceBackend{
@@ -327,16 +367,41 @@ func TestStartError(t *testing.T) {
 	require.ErrorContains(t, err, "empty configuration")
 }
 
-func TestMetricsStorageStartError(t *testing.T) {
-	ext := makeStorageExtension(t, &Config{
-		MetricBackends: map[string]MetricBackend{
-			"foo": {
-				Prometheus: &promCfg.Configuration{},
+func TestMetricStorageStartError(t *testing.T) {
+	expectedError := "failed to initialize metrics storage 'foo'"
+	tests := []struct {
+		name   string
+		config *Config
+	}{
+		{
+			name: "Prometheus backend initialization error",
+			config: &Config{
+				MetricBackends: map[string]MetricBackend{
+					"foo": {
+						Prometheus: &promCfg.Configuration{},
+					},
+				},
 			},
 		},
-	})
-	err := ext.Start(context.Background(), componenttest.NewNopHost())
-	require.ErrorContains(t, err, "failed to initialize metrics storage 'foo'")
+		{
+			name: "Elasticsearch backend initialization error",
+			config: &Config{
+				MetricBackends: map[string]MetricBackend{
+					"foo": {
+						Elasticsearch: &esCfg.Configuration{},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ext := makeStorageExtension(t, tt.config)
+			err := ext.Start(context.Background(), componenttest.NewNopHost())
+			require.ErrorContains(t, err, expectedError)
+		})
+	}
 }
 
 func testElasticsearchOrOpensearch(t *testing.T, cfg TraceBackend) {
@@ -352,16 +417,7 @@ func testElasticsearchOrOpensearch(t *testing.T, cfg TraceBackend) {
 }
 
 func TestXYZsearch(t *testing.T) {
-	versionResponse, err := json.Marshal(map[string]any{
-		"Version": map[string]any{
-			"Number": "7",
-		},
-	})
-	require.NoError(t, err)
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
-		w.Write(versionResponse)
-	}))
-	defer server.Close()
+	server := setupMockServer(t, getVersionResponse(t), http.StatusOK)
 	t.Run("Elasticsearch", func(t *testing.T) {
 		testElasticsearchOrOpensearch(t, TraceBackend{
 			Elasticsearch: &esCfg.Configuration{
diff --git a/internal/storage/elasticsearch/config/config_test.go b/internal/storage/elasticsearch/config/config_test.go
index 72af30e6..ec8385f5 100644
--- a/internal/storage/elasticsearch/config/config_test.go
+++ b/internal/storage/elasticsearch/config/config_test.go
@@ -4,6 +4,7 @@
 package config
 
 import (
+	"context"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -416,7 +417,7 @@ func TestNewClient(t *testing.T) {
 			logger := zap.NewNop()
 			metricsFactory := metrics.NullFactory
 			config := test.config
-			client, err := NewClient(config, logger, metricsFactory)
+			client, err := NewClient(context.Background(), config, logger, metricsFactory)
 			if test.expectedError {
 				require.Error(t, err)
 				require.Nil(t, client)
diff --git a/internal/storage/metricstore/elasticsearch/factory_test.go b/internal/storage/metricstore/elasticsearch/factory_test.go
index bd734697..35025444 100644
--- a/internal/storage/metricstore/elasticsearch/factory_test.go
+++ b/internal/storage/metricstore/elasticsearch/factory_test.go
@@ -4,7 +4,9 @@
 package elasticsearch
 
 import (
-	"net"
+	"context"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -17,36 +19,61 @@ import (
 
 var _ storage.MetricStoreFactory = new(Factory)
 
-func TestCreateMetricsReader(t *testing.T) {
-	listener, err := net.Listen("tcp", "localhost:")
-	require.NoError(t, err)
-	assert.NotNil(t, listener)
-	defer listener.Close()
+// mockESServerResponse simulates a successful Elasticsearch version response.
+var mockESServerResponse = []byte(`
+{
+    "version": {
+       "number": "6.8.0"
+    }
+}
+`)
+
+// setupMockServer creates a mock HTTP server with the specified response and status code.
+func setupMockServer(t *testing.T, response []byte, statusCode int) *httptest.Server {
+	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(statusCode)
+		w.Write(response)
+	}))
+	require.NotNil(t, mockServer)
+	t.Cleanup(mockServer.Close)
+
+	return mockServer
+}
 
-	cfg := config.Configuration{
-		Servers: []string{"http://" + listener.Addr().String()},
+// newTestFactoryConfig creates a default configuration for testing.
+func newTestFactoryConfig(serverURL string) config.Configuration {
+	return config.Configuration{
+		Servers:  []string{serverURL},
+		LogLevel: "debug",
 	}
-	f, err := NewFactory(cfg, telemetry.NoopSettings())
+}
+
+func TestCreateMetricsReader(t *testing.T) {
+	server := setupMockServer(t, mockESServerResponse, http.StatusOK)
+	cfg := newTestFactoryConfig(server.URL)
+	f, err := NewFactory(context.Background(), cfg, telemetry.NoopSettings())
 	require.NoError(t, err)
 	require.NotNil(t, f)
+	defer require.NoError(t, f.Close())
 
 	reader, err := f.CreateMetricsReader()
-
 	require.NoError(t, err)
 	assert.NotNil(t, reader)
 }
 
 func TestNewFactory(t *testing.T) {
+	mockServer := setupMockServer(t, mockESServerResponse, http.StatusOK)
 	tests := []struct {
 		name        string
 		cfg         config.Configuration
+		response    []byte
+		statusCode  int
 		expectedErr bool
 	}{
 		{
-			name: "valid config",
-			cfg: config.Configuration{
-				Servers: []string{"http://localhost:9200"},
-			},
+			name:        "valid config",
+			cfg:         newTestFactoryConfig(mockServer.URL),
 			expectedErr: false,
 		},
 		{
@@ -56,17 +83,35 @@ func TestNewFactory(t *testing.T) {
 			},
 			expectedErr: true,
 		},
+		{
+			name:        "invalid config - malformed server URL",
+			cfg:         newTestFactoryConfig("://malformed-url"),
+			expectedErr: true,
+		},
+		{
+			name:        "ping failure for version detection",          // New situation to test error from create es client
+			cfg:         newTestFactoryConfig("http://localhost:9090"), // Overridden by mock server
+			response:    []byte(`{"error": "internal server error"}`),
+			statusCode:  http.StatusInternalServerError,
+			expectedErr: true,
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			telset := telemetry.NoopSettings()
-			_, err := NewFactory(tt.cfg, telset)
+			if tt.response != nil && tt.statusCode != 0 {
+				server := setupMockServer(t, tt.response, tt.statusCode)
+				tt.cfg.Servers = []string{server.URL}
+			}
+			f, err := NewFactory(context.Background(), tt.cfg, telemetry.NoopSettings())
 
 			if tt.expectedErr {
 				require.Error(t, err)
+				require.Nil(t, f)
 			} else {
 				require.NoError(t, err)
+				require.NotNil(t, f)
+				require.NoError(t, f.Close())
 			}
 		})
 	}
diff --git a/internal/storage/metricstore/elasticsearch/reader_test.go b/internal/storage/metricstore/elasticsearch/reader_test.go
index 4ccb8588..eff8748f 100644
--- a/internal/storage/metricstore/elasticsearch/reader_test.go
+++ b/internal/storage/metricstore/elasticsearch/reader_test.go
@@ -5,7 +5,8 @@ package elasticsearch
 
 import (
 	"context"
-	"net"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 	"time"
 
@@ -16,37 +17,70 @@ import (
 	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/zap"
 
+	"github.com/jaegertracing/jaeger/internal/metrics"
+	es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch"
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
 	"github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore"
+	"github.com/jaegertracing/jaeger/internal/telemetry"
 )
 
-func tracerProvider(t *testing.T) (trace.TracerProvider, *tracetest.InMemoryExporter, func()) {
+// mockServerResponse simulates a basic Elasticsearch version response.
+var mockServerResponse = []byte(`
+{
+    "version": {
+       "number": "6.8.0"
+    }
+}
+`)
+
+// tracerProvider creates a new OpenTelemetry TracerProvider for testing.
+func tracerProvider(t *testing.T) trace.TracerProvider {
 	exporter := tracetest.NewInMemoryExporter()
 	tp := sdktrace.NewTracerProvider(
 		sdktrace.WithSampler(sdktrace.AlwaysSample()),
 		sdktrace.WithSyncer(exporter),
 	)
-	closer := func() {
+	t.Cleanup(func() {
 		require.NoError(t, tp.Shutdown(context.Background()))
-	}
-	return tp, exporter, closer
+	})
+	return tp
 }
 
-func TestGetLatencies(t *testing.T) {
+func clientProvider(t *testing.T, c *config.Configuration, logger *zap.Logger, metricsFactory metrics.Factory) es.Client {
+	client, err := config.NewClient(context.Background(), c, logger, metricsFactory)
+	require.NoError(t, err)
+	require.NotNil(t, client)
+
+	t.Cleanup(func() {
+		require.NoError(t, client.Close())
+	})
+	return client
+}
+
+// setupMetricsReader provides a common setup for tests requiring a MetricsReader.
+func setupMetricsReader(t *testing.T) *MetricsReader {
 	logger := zap.NewNop()
-	tracer, _, closer := tracerProvider(t)
-	defer closer()
+	tracer := tracerProvider(t)
+
+	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(mockServerResponse)
+	}))
+	t.Cleanup(mockServer.Close)
+	cfg := config.Configuration{
+		Servers:  []string{mockServer.URL},
+		LogLevel: "debug",
+	}
 
-	listener, err := net.Listen("tcp", "localhost:")
-	require.NoError(t, err)
-	assert.NotNil(t, listener)
-	defer listener.Close()
+	client := clientProvider(t, &cfg, logger, telemetry.NoopSettings().Metrics)
+	reader := NewMetricsReader(client, logger, tracer)
 
-	reader, err := NewMetricsReader(config.Configuration{
-		Servers: []string{"http://" + listener.Addr().String()},
-	}, logger, tracer)
-	require.NoError(t, err)
 	require.NotNil(t, reader)
+	return reader
+}
+
+func TestGetLatencies(t *testing.T) {
+	reader := setupMetricsReader(t)
 
 	qParams := &metricstore.LatenciesQueryParameters{}
 	r, err := reader.GetLatencies(context.Background(), qParams)
@@ -56,20 +90,7 @@ func TestGetLatencies(t *testing.T) {
 }
 
 func TestGetCallRates(t *testing.T) {
-	logger := zap.NewNop()
-	tracer, _, closer := tracerProvider(t)
-	defer closer()
-	listener, err := net.Listen("tcp", "localhost:")
-	require.NoError(t, err)
-	assert.NotNil(t, listener)
-	defer listener.Close()
-
-	reader, err := NewMetricsReader(config.Configuration{
-		Servers: []string{"http://" + listener.Addr().String()},
-	}, logger, tracer)
-
-	require.NoError(t, err)
-	require.NotNil(t, reader)
+	reader := setupMetricsReader(t)
 
 	qParams := &metricstore.CallRateQueryParameters{}
 	r, err := reader.GetCallRates(context.Background(), qParams)
@@ -79,21 +100,7 @@ func TestGetCallRates(t *testing.T) {
 }
 
 func TestGetErrorRates(t *testing.T) {
-	logger := zap.NewNop()
-	tracer, _, closer := tracerProvider(t)
-	defer closer()
-
-	listener, err := net.Listen("tcp", "localhost:")
-	require.NoError(t, err)
-	assert.NotNil(t, listener)
-	defer listener.Close()
-
-	reader, err := NewMetricsReader(config.Configuration{
-		Servers: []string{"http://" + listener.Addr().String()},
-	}, logger, tracer)
-
-	require.NoError(t, err)
-	require.NotNil(t, reader)
+	reader := setupMetricsReader(t)
 
 	qParams := &metricstore.ErrorRateQueryParameters{}
 	r, err := reader.GetErrorRates(context.Background(), qParams)
@@ -103,22 +110,9 @@ func TestGetErrorRates(t *testing.T) {
 }
 
 func TestGetMinStepDuration(t *testing.T) {
-	params := metricstore.MinStepDurationQueryParameters{}
-	logger := zap.NewNop()
-	tracer, _, closer := tracerProvider(t)
-	defer closer()
-
-	listener, err := net.Listen("tcp", "localhost:")
-	require.NoError(t, err)
-	assert.NotNil(t, listener)
-	defer listener.Close()
-
-	reader, err := NewMetricsReader(config.Configuration{
-		Servers: []string{"http://" + listener.Addr().String()},
-	}, logger, tracer)
-	require.NoError(t, err)
-	require.NotNil(t, reader)
+	reader := setupMetricsReader(t)
 
+	params := metricstore.MinStepDurationQueryParameters{}
 	minStep, err := reader.GetMinStepDuration(context.Background(), &params)
 	require.NoError(t, err)
 	assert.Equal(t, time.Millisecond, minStep)
diff --git a/internal/storage/v1/elasticsearch/factory_test.go b/internal/storage/v1/elasticsearch/factory_test.go
index b032a04d..803c6f70 100644
--- a/internal/storage/v1/elasticsearch/factory_test.go
+++ b/internal/storage/v1/elasticsearch/factory_test.go
@@ -209,7 +209,7 @@ func TestCreateTemplates(t *testing.T) {
 	for _, test := range tests {
 		f := FactoryBase{}
 		mockClient := &mocks.Client{}
-		f.newClientFn = func(_ *escfg.Configuration, _ *zap.Logger, _ metrics.Factory) (es.Client, error) {
+		f.newClientFn = func(_ context.Context, _ *escfg.Configuration, _ *zap.Logger, _ metrics.Factory) (es.Client, error) {
 			return mockClient, nil
 		}
 		f.logger = zaptest.NewLogger(t)
@@ -228,7 +228,7 @@ func TestCreateTemplates(t *testing.T) {
 			},
 		}}
 		f.tracer = otel.GetTracerProvider()
-		client, err := f.newClientFn(&escfg.Configuration{}, zaptest.NewLogger(t), metrics.NullFactory)
+		client, err := f.newClientFn(context.Background(), &escfg.Configuration{}, zaptest.NewLogger(t), metrics.NullFactory)
 		require.NoError(t, err)
 		f.client.Store(&client)
 		f.templateBuilder = es.TextTemplateBuilder{}
diff --git a/internal/storage/v2/api/tracestore/tracestoremetrics/package_test.go b/internal/storage/v2/api/tracestore/tracestoremetrics/package_test.go
new file mode 100644
index 00000000..8e5a05dc
--- /dev/null
+++ b/internal/storage/v2/api/tracestore/tracestoremetrics/package_test.go
@@ -0,0 +1,14 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestoremetrics
+
+import (
+	"testing"
+
+	"github.com/jaegertracing/jaeger/internal/testutils"
+)
+
+func TestMain(m *testing.M) {
+	testutils.VerifyGoLeaks(m)
+}
diff --git a/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics_test.go b/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics_test.go
new file mode 100644
index 00000000..6e89693a
--- /dev/null
+++ b/internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics_test.go
@@ -0,0 +1,176 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestoremetrics
+
+import (
+	"context"
+	"iter"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+
+	"github.com/jaegertracing/jaeger/internal/metricstest"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks"
+)
+
+func TestSuccessfulUnderlyingCalls(t *testing.T) {
+	mf := metricstest.NewFactory(0)
+
+	mockReader := mocks.Reader{}
+	mrs := NewReaderDecorator(&mockReader, mf)
+	traces := []ptrace.Traces{ptrace.NewTraces(), ptrace.NewTraces()}
+	mockReader.On("GetServices", context.Background()).Return([]string{"service-x"}, nil)
+	mrs.GetServices(context.Background())
+	operationQuery := tracestore.OperationQueryParams{ServiceName: "something"}
+	mockReader.On("GetOperations", context.Background(), operationQuery).
+		Return([]tracestore.Operation{{}}, nil)
+	mrs.GetOperations(context.Background(), operationQuery)
+	mockReader.On("GetTraces", context.Background(), []tracestore.GetTraceParams{{}}).Return(emptyIter[ptrace.Traces](traces, nil))
+	count := 0
+	for range mrs.GetTraces(context.Background(), tracestore.GetTraceParams{}) {
+		if count != 0 {
+			break
+		}
+		count++
+	}
+	mockReader.On("FindTraces", context.Background(), tracestore.TraceQueryParams{}).
+		Return(emptyIter[ptrace.Traces](traces, nil))
+	count = 0
+	for range mrs.FindTraces(context.Background(), tracestore.TraceQueryParams{}) {
+		if count != 0 {
+			break
+		}
+		count++
+	}
+	mockReader.On("FindTraceIDs", context.Background(), tracestore.TraceQueryParams{}).
+		Return(emptyIter[tracestore.FoundTraceID]([]tracestore.FoundTraceID{{TraceID: [16]byte{}}, {TraceID: [16]byte{}}}, nil))
+	count = 0
+	for range mrs.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{}) {
+		if count != 0 {
+			break
+		}
+		count++
+	}
+	counters, gauges := mf.Snapshot()
+	expected := map[string]int64{
+		"requests|operation=get_operations|result=ok":  1,
+		"requests|operation=get_operations|result=err": 0,
+		"requests|operation=get_trace|result=ok":       1,
+		"requests|operation=get_trace|result=err":      0,
+		"requests|operation=find_traces|result=ok":     1,
+		"requests|operation=find_traces|result=err":    0,
+		"requests|operation=find_trace_ids|result=ok":  1,
+		"requests|operation=find_trace_ids|result=err": 0,
+		"requests|operation=get_services|result=ok":    1,
+		"requests|operation=get_services|result=err":   0,
+		"responses|operation=get_trace":                2,
+		"responses|operation=find_traces":              2,
+		"responses|operation=find_trace_ids":           2,
+		"responses|operation=get_operations":           1,
+		"responses|operation=get_services":             1,
+	}
+
+	existingKeys := []string{
+		"latency|operation=get_operations|result=ok.P50",
+		"latency|operation=find_traces|result=ok.P50", // this is not exhaustive
+	}
+	nonExistentKeys := []string{
+		"latency|operation=get_operations|result=err.P50",
+	}
+
+	checkExpectedExistingAndNonExistentCounters(t, counters, expected, gauges, existingKeys, nonExistentKeys)
+}
+
+func checkExpectedExistingAndNonExistentCounters(t *testing.T,
+	actualCounters,
+	expectedCounters,
+	actualGauges map[string]int64,
+	existingKeys,
+	nonExistentKeys []string,
+) {
+	for k, v := range expectedCounters {
+		assert.Equal(t, v, actualCounters[k], k)
+	}
+
+	for _, k := range existingKeys {
+		_, ok := actualGauges[k]
+		assert.True(t, ok, k)
+	}
+
+	for _, k := range nonExistentKeys {
+		_, ok := actualGauges[k]
+		assert.False(t, ok, k)
+	}
+}
+
+func TestFailingUnderlyingCalls(t *testing.T) {
+	mf := metricstest.NewFactory(0)
+
+	mockReader := mocks.Reader{}
+	mrs := NewReaderDecorator(&mockReader, mf)
+	returningErr := assert.AnError
+	mockReader.On("GetServices", context.Background()).
+		Return(nil, returningErr)
+	mrs.GetServices(context.Background())
+	operationQuery := tracestore.OperationQueryParams{ServiceName: "something"}
+	mockReader.On("GetOperations", context.Background(), operationQuery).
+		Return(nil, returningErr)
+	mrs.GetOperations(context.Background(), operationQuery)
+	mockReader.On("GetTraces", context.Background(), []tracestore.GetTraceParams{{}}).
+		Return(emptyIter[ptrace.Traces](nil, returningErr))
+	//nolint:revive
+	for range mrs.GetTraces(context.Background(), tracestore.GetTraceParams{}) {
+		// It is necessary to range the iter to emit metrics, therefore this empty loop is present
+	}
+	mockReader.On("FindTraces", context.Background(), tracestore.TraceQueryParams{}).
+		Return(emptyIter[ptrace.Traces](nil, returningErr))
+	//nolint:revive
+	for range mrs.FindTraces(context.Background(), tracestore.TraceQueryParams{}) {
+	}
+	mockReader.On("FindTraceIDs", context.Background(), tracestore.TraceQueryParams{}).
+		Return(emptyIter[tracestore.FoundTraceID](nil, returningErr))
+	//nolint:revive
+	for range mrs.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{}) {
+	}
+	counters, gauges := mf.Snapshot()
+	expecteds := map[string]int64{
+		"requests|operation=get_operations|result=ok":  0,
+		"requests|operation=get_operations|result=err": 1,
+		"requests|operation=get_trace|result=ok":       0,
+		"requests|operation=get_trace|result=err":      1,
+		"requests|operation=find_traces|result=ok":     0,
+		"requests|operation=find_traces|result=err":    1,
+		"requests|operation=find_trace_ids|result=ok":  0,
+		"requests|operation=find_trace_ids|result=err": 1,
+		"requests|operation=get_services|result=ok":    0,
+		"requests|operation=get_services|result=err":   1,
+	}
+
+	existingKeys := []string{
+		"latency|operation=get_operations|result=err.P50",
+	}
+
+	nonExistentKeys := []string{
+		"latency|operation=get_operations|result=ok.P50",
+		"latency|operation=query|result=ok.P50", // this is not exhaustive
+	}
+
+	checkExpectedExistingAndNonExistentCounters(t, counters, expecteds, gauges, existingKeys, nonExistentKeys)
+}
+
+func emptyIter[T any](td []T, err error) iter.Seq2[[]T, error] {
+	return func(yield func([]T, error) bool) {
+		if err != nil {
+			yield(nil, err)
+			return
+		}
+		for _, t := range td {
+			if !yield([]T{t}, nil) {
+				return
+			}
+		}
+	}
+}
diff --git a/internal/storage/v2/elasticsearch/factory_test.go b/internal/storage/v2/elasticsearch/factory_test.go
index 376adc03..6b693a4f 100644
--- a/internal/storage/v2/elasticsearch/factory_test.go
+++ b/internal/storage/v2/elasticsearch/factory_test.go
@@ -29,7 +29,7 @@ var mockEsServerResponse = []byte(`
 func TestNewFactory(t *testing.T) {
 	cfg := escfg.Configuration{}
 	coreFactory := getTestingFactoryBase(t, &cfg)
-	f := &Factory{coreFactory: coreFactory, config: cfg}
+	f := &Factory{coreFactory: coreFactory, config: cfg, metricsFactory: metrics.NullFactory}
 	_, err := f.CreateTraceReader()
 	require.NoError(t, err)
 	_, err = f.CreateTraceWriter()