Repository: jaegertracing/jaeger

Issue:
Implement v2 Factory For Remote Storage
Now that we have the gRPC client in place for interacting with the remote storage, we want to implement the factory for gRPC that will be instantiated by the storage extension

Hints from issue comments:

Solution patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1983839f..97950f46 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,44 +85,38 @@ These are general guidelines on how to organize source code in this repository.
 ```
 github.com/jaegertracing/jaeger
   cmd/                      - All binaries go here
-    agent/
-      app/                  - The actual code for the binary
-      main.go
-    collector/
-      app/                  - The actual code for the binary
-      main.go
+    all-in-one/             - Jaeger all-in-one application, designed for quick local testing
+    jaeger/                 - Jaeger V2 binary
+    collector/              - Component to receive spans (from agents or directly from clients) and saves them in Trace Storage
+    ingester/               - Component to read spans from Kafka topic and save them to storage
+    query/                  - Component to serve Jaeger UI and an API that retrieves traces from storage
+    remote-storage/         - Component to enable sharing single-node storage implementations like memstore by implementing Remote Storage API
+    anonymizer/             - Utility to anonymize traces from Jaeger query and save to file
+    tracegen/               - Utility to generate a steady flow of simple traces
+    es-index-cleaner/       - Utility to purge old indices from Elasticsearch
+    es-rollover/            - Utility to manage Elastic Search indices
   crossdock/                - Cross-repo integration test configuration
   examples/
-      hotrod/               - Demo application that demonstrates the use of tracing instrumentation
+    grafana-integration/    - Demo application that combine Jaeger, Grafana, Loki, Prometheus to demonstrate logs, metrics and traces correlation
+    hotrod/                 - Demo application that demonstrates the use of tracing instrumentation
+  docker-compose/           - Docker-compose recipes to simulate different Jaeger deployments
+    kafka/                  - Jaeger depoyment utilizing collector-Kafka-injester pipeline
+    monitor/                - Service Performance Monitoring (SPM) Development/Demo Environment
   idl/                      - (submodule) https://github.com/jaegertracing/jaeger-idl
   jaeger-ui/                - (submodule) https://github.com/jaegertracing/jaeger-ui
-  model/                    - Where models are kept, e.g. Process, Span, Trace
-  pkg/                      - (See Note 1)
-  plugin/                   - Swappable implementations of various components
-    storage/
-      cassandra/            - Cassandra implementations of storage APIs
-        .                   - Shared Cassandra stuff
-        spanstore/          - SpanReader / SpanWriter implementations
-        dependencystore/
-      elasticsearch/        - ES implementations of storage APIs
+  internal/                 - Internal modules that make up Jaeger
+    storage/                - Define and implement Trace/Metrics Storage interface 
+      metricstore/          - Define and implement Metrics Storage interface
+        prometheus/         - Prometheus implementation of Metrics Storage
+      v1/                   - V1 Trace Storage interfaces and implementations
+        memory/             - In-memory implementation
+        elasticsearch/      - ElasticSearch implementation
+      v2/                   - V2 Trace Storage interfaces and implementation
   scripts/                  - Miscellaneous project scripts, e.g. github action and license update script
-  storage/
-    spanstore/              - SpanReader / SpanWriter interfaces
-    dependencystore/
-  thrift-gen/               - Generated Thrift types
-    agent/
-    jaeger/
-    sampling/
-    zipkincore/
   go.mod                    - Go module file to track dependencies
+  Makefile                  - Define various recipes to automate build, test, and deployment tasks
 ```
 
-- Note 1: `pkg` is a collection of utility packages used by the Jaeger components
-  without being specific to its internals. Utility packages are kept separate from
-  the Jaeger core codebase to keep it as small and concise as possible. If some
-  utilities grow larger and their APIs stabilize, they may be moved to their own
-  repository, to facilitate re-use by other projects.
-
 ## Imports grouping
 
 This project follows the following pattern for grouping imports in Go files:
diff --git a/internal/storage/v2/grpc/config.go b/internal/storage/v2/grpc/config.go
new file mode 100644
index 00000000..1e971015
--- /dev/null
+++ b/internal/storage/v2/grpc/config.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package grpc
+
+import (
+	"time"
+
+	"go.opentelemetry.io/collector/config/configgrpc"
+	"go.opentelemetry.io/collector/exporter/exporterhelper"
+
+	"github.com/jaegertracing/jaeger/internal/tenancy"
+)
+
+type Config struct {
+	Tenancy                      tenancy.Options `mapstructure:"multi_tenancy"`
+	configgrpc.ClientConfig      `mapstructure:",squash"`
+	exporterhelper.TimeoutConfig `mapstructure:",squash"`
+}
+
+func DefaultConfig() Config {
+	return Config{
+		TimeoutConfig: exporterhelper.TimeoutConfig{
+			Timeout: time.Duration(5 * time.Second),
+		},
+	}
+}
diff --git a/internal/storage/v2/grpc/factory.go b/internal/storage/v2/grpc/factory.go
new file mode 100644
index 00000000..1f57cdf4
--- /dev/null
+++ b/internal/storage/v2/grpc/factory.go
@@ -0,0 +1,147 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package grpc
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/config/configgrpc"
+	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+	"go.opentelemetry.io/otel/trace"
+	"go.opentelemetry.io/otel/trace/noop"
+	"google.golang.org/grpc"
+
+	"github.com/jaegertracing/jaeger/internal/bearertoken"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore"
+	"github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore"
+	"github.com/jaegertracing/jaeger/internal/telemetry"
+	"github.com/jaegertracing/jaeger/internal/tenancy"
+)
+
+var (
+	_ io.Closer          = (*Factory)(nil)
+	_ tracestore.Factory = (*Factory)(nil)
+	_ depstore.Factory   = (*Factory)(nil)
+)
+
+type Factory struct {
+	telset telemetry.Settings
+	config Config
+	// readerConn is the gRPC connection used for reading data from the remote storage backend.
+	// It is safe for this connection to have instrumentation enabled without
+	// the risk of recursively generating traces.
+	readerConn *grpc.ClientConn
+	// writerConn is the gRPC connection used for writing data to the remote storage backend.
+	// This connection should not have instrumentation enabled to avoid recursively generating traces.
+	writerConn *grpc.ClientConn
+}
+
+// NewFactory initializes a new gRPC (remote) storage backend.
+func NewFactory(
+	cfg Config,
+	telset telemetry.Settings,
+) (*Factory, error) {
+	f := &Factory{
+		telset: telset,
+		config: cfg,
+	}
+
+	readerTelset := getTelset(f.telset, f.telset.TracerProvider)
+	writerTelset := getTelset(f.telset, noop.NewTracerProvider())
+	newClientFn := func(telset component.TelemetrySettings, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) {
+		clientOpts := make([]configgrpc.ToClientConnOption, 0)
+		for _, opt := range opts {
+			clientOpts = append(clientOpts, configgrpc.WithGrpcDialOption(opt))
+		}
+		return f.config.ToClientConn(context.Background(), f.telset.Host, telset, clientOpts...)
+	}
+
+	if err := f.initializeConnections(readerTelset, writerTelset, newClientFn); err != nil {
+		return nil, err
+	}
+
+	return f, nil
+}
+
+func (f *Factory) CreateTraceReader() (tracestore.Reader, error) {
+	return NewTraceReader(f.readerConn), nil
+}
+
+func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) {
+	return NewTraceWriter(f.writerConn), nil
+}
+
+func (f *Factory) CreateDependencyReader() (depstore.Reader, error) {
+	return NewDependencyReader(f.readerConn), nil
+}
+
+func (f *Factory) Close() error {
+	var errs []error
+	if f.readerConn != nil {
+		errs = append(errs, f.readerConn.Close())
+	}
+	if f.writerConn != nil {
+		errs = append(errs, f.writerConn.Close())
+	}
+	return errors.Join(errs...)
+}
+
+func getTelset(
+	telset telemetry.Settings,
+	tracerProvider trace.TracerProvider,
+) component.TelemetrySettings {
+	return component.TelemetrySettings{
+		Logger:         telset.Logger,
+		TracerProvider: tracerProvider,
+		MeterProvider:  telset.MeterProvider,
+	}
+}
+
+type newClientFn func(telset component.TelemetrySettings, opts ...grpc.DialOption) (*grpc.ClientConn, error)
+
+func (f *Factory) initializeConnections(
+	readerTelset, writerTelset component.TelemetrySettings,
+	newClient newClientFn,
+) error {
+	if f.config.Auth != nil {
+		return errors.New("authenticator is not supported")
+	}
+
+	unaryInterceptors := []grpc.UnaryClientInterceptor{bearertoken.NewUnaryClientInterceptor()}
+	streamInterceptors := []grpc.StreamClientInterceptor{bearertoken.NewStreamClientInterceptor()}
+
+	if tenancyMgr := tenancy.NewManager(&f.config.Tenancy); tenancyMgr.Enabled {
+		unaryInterceptors = append(unaryInterceptors, tenancy.NewClientUnaryInterceptor(tenancyMgr))
+		streamInterceptors = append(streamInterceptors, tenancy.NewClientStreamInterceptor(tenancyMgr))
+	}
+
+	baseOpts := []grpc.DialOption{
+		grpc.WithChainUnaryInterceptor(unaryInterceptors...),
+		grpc.WithChainStreamInterceptor(streamInterceptors...),
+	}
+
+	createConn := func(telset component.TelemetrySettings) (*grpc.ClientConn, error) {
+		opts := append(baseOpts, grpc.WithStatsHandler(
+			otelgrpc.NewClientHandler(otelgrpc.WithTracerProvider(telset.TracerProvider)),
+		))
+		return newClient(telset, opts...)
+	}
+
+	readerConn, err := createConn(readerTelset)
+	if err != nil {
+		return fmt.Errorf("error creating reader client connection: %w", err)
+	}
+	writerConn, err := createConn(writerTelset)
+	if err != nil {
+		return fmt.Errorf("error creating writer client connection: %w", err)
+	}
+
+	f.readerConn, f.writerConn = readerConn, writerConn
+
+	return nil
+}
diff --git a/pkg/README.md b/pkg/README.md
deleted file mode 100644
index 0d500737..00000000
--- a/pkg/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-The collection of utility packages used by the Jaeger components without being specific to its internals.
-
-Utility packages are kept separate from the Jaeger core codebase to keep it as small and concise as possible. If some utilities grow larger and their APIs stabilize, they may be moved to their own repository, to facilitate re-use by other projects. However that is not the priority.
-
diff --git a/pkg/doc.go b/pkg/doc.go
deleted file mode 100644
index a21da114..00000000
--- a/pkg/doc.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2019 The Jaeger Authors.
-// Copyright (c) 2017 Uber Technologies, Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-// Package pkg is the collection of utility packages used by the Jaeger components without being specific to its internals.
-//
-// Utility packages are kept separate from the Jaeger core codebase to keep it as small and concise as possible. If some utilities grow larger and their APIs stabilize, they may be moved to their own repository, to facilitate re-use by other projects. However that is not the priority.
-package pkg



Test patch:
diff --git a/internal/storage/v2/grpc/config_test.go b/internal/storage/v2/grpc/config_test.go
new file mode 100644
index 00000000..fecb0fe3
--- /dev/null
+++ b/internal/storage/v2/grpc/config_test.go
@@ -0,0 +1,15 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package grpc
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefaultConfig(t *testing.T) {
+	cfg := DefaultConfig()
+	require.NotEmpty(t, cfg.Timeout)
+}
diff --git a/internal/storage/v2/grpc/factory_test.go b/internal/storage/v2/grpc/factory_test.go
new file mode 100644
index 00000000..e85a52b3
--- /dev/null
+++ b/internal/storage/v2/grpc/factory_test.go
@@ -0,0 +1,101 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package grpc
+
+import (
+	"net"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/config/configauth"
+	"go.opentelemetry.io/collector/config/configgrpc"
+	"go.opentelemetry.io/collector/exporter/exporterhelper"
+	"google.golang.org/grpc"
+
+	"github.com/jaegertracing/jaeger/internal/telemetry"
+	"github.com/jaegertracing/jaeger/internal/tenancy"
+)
+
+func TestNewFactory_NonEmptyAuthenticator(t *testing.T) {
+	cfg := &Config{
+		ClientConfig: configgrpc.ClientConfig{
+			Auth: &configauth.Authentication{},
+		},
+	}
+	_, err := NewFactory(*cfg, telemetry.NoopSettings())
+	require.ErrorContains(t, err, "authenticator is not supported")
+}
+
+func TestNewFactory(t *testing.T) {
+	lis, err := net.Listen("tcp", ":0")
+	require.NoError(t, err, "failed to listen")
+
+	s := grpc.NewServer()
+
+	startServer(t, s, lis)
+
+	cfg := Config{
+		ClientConfig: configgrpc.ClientConfig{
+			Endpoint: lis.Addr().String(),
+		},
+		TimeoutConfig: exporterhelper.TimeoutConfig{
+			Timeout: 1 * time.Second,
+		},
+		Tenancy: tenancy.Options{
+			Enabled: true,
+		},
+	}
+	telset := telemetry.NoopSettings()
+	f, err := NewFactory(cfg, telset)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+}
+
+func TestFactory(t *testing.T) {
+	lis, err := net.Listen("tcp", ":0")
+	require.NoError(t, err, "failed to listen")
+
+	s := grpc.NewServer()
+
+	conn := startServer(t, s, lis)
+	f := &Factory{
+		readerConn: conn,
+	}
+
+	t.Run("CreateTraceReader", func(t *testing.T) {
+		tr, err := f.CreateTraceReader()
+		require.NoError(t, err)
+		require.NotNil(t, tr)
+	})
+
+	t.Run("CreateTraceWriter", func(t *testing.T) {
+		tr, err := f.CreateTraceWriter()
+		require.NoError(t, err)
+		require.NotNil(t, tr)
+	})
+
+	t.Run("CreateDependencyReader", func(t *testing.T) {
+		tr, err := f.CreateDependencyReader()
+		require.NoError(t, err)
+		require.NotNil(t, tr)
+	})
+}
+
+func TestInitializeConnections_ClientError(t *testing.T) {
+	f, err := NewFactory(Config{
+		ClientConfig: configgrpc.ClientConfig{
+			Endpoint: ":0",
+		},
+	}, telemetry.NoopSettings())
+	require.NoError(t, err)
+	t.Cleanup(func() { require.NoError(t, f.Close()) })
+	newClientFn := func(_ component.TelemetrySettings, _ ...grpc.DialOption) (conn *grpc.ClientConn, err error) {
+		return nil, assert.AnError
+	}
+	err = f.initializeConnections(component.TelemetrySettings{}, component.TelemetrySettings{}, newClientFn)
+	assert.ErrorContains(t, err, "error creating reader client connection")
+}
diff --git a/pkg/empty_test.go b/pkg/empty_test.go
deleted file mode 100644
index d3b6116a..00000000
--- a/pkg/empty_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2020 The Jaeger Authors.
-// SPDX-License-Identifier: Apache-2.0
-
-package pkg
-
-import (
-	"testing"
-
-	"github.com/jaegertracing/jaeger/internal/testutils"
-)
-
-func TestDummy(*testing.T) {
-	// This is a dummy test in the root package.
-	// Without it `go test -v .` prints "testing: warning: no tests to run".
-}
-
-func TestMain(m *testing.M) {
-	testutils.VerifyGoLeaks(m)
-}