jaegertracing/jaeger · error

service/operation reads require a searcher, but this storage

Error message

service/operation reads require a searcher, but this storage was constructed for write-only use

What it means

errNoSearcher is an unexported error returned by the read methods (getServices/getOperations) of ServiceOperationStorage when the storage was constructed without a searcher, i.e. in write-only mode as described on NewServiceOperationStorage. It converts what would otherwise be a nil-pointer panic on the searcher into a clear, actionable configuration error.

Source

Thrown at internal/storage/v2/elasticsearch/tracestore/core/service_operation.go:38

	"github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/tracestore/core/dbmodel"
)

const (
	serviceName = "serviceName"

	// serviceCacheTTLDefault is how long a written service:operation pair is
	// remembered when the configuration does not say.
	serviceCacheTTLDefault = 12 * time.Hour

	operationsAggregation = "distinct_operations"
	servicesAggregation   = "distinct_services"
)

// errNoSearcher is returned by the read methods (getServices/getOperations) when
// the storage was constructed without a searcher — the write-only case described
// on NewServiceOperationStorage. It turns that misconfiguration into a clear
// error rather than a nil-pointer panic.
var errNoSearcher = errors.New("service/operation reads require a searcher, but this storage was constructed for write-only use")

// ServiceOperationStorage stores service to operation pairs.
type ServiceOperationStorage struct {
	searcher     esclient.Searcher
	logger       *zap.Logger
	serviceCache cache.Cache
}

// NewServiceOperationStorage returns a new ServiceOperationStorage. searcher is
// used only by the read methods (getServices/getOperations); a write-only instance
// (the SpanWriter) may pass a nil searcher. The write side builds documents via
// toUpsertItem and commits the cache via commitToCache — it does not write directly.
// A cacheTTL of zero selects serviceCacheTTLDefault.
func NewServiceOperationStorage(
	searcher esclient.Searcher,
	logger *zap.Logger,
	cacheTTL time.Duration,
) *ServiceOperationStorage {

View on GitHub (pinned to 806f444784)

Solutions

  1. Construct the storage with a non-nil searcher if you need service/operation reads.
  2. Use a separate, read-capable storage instance for the query path.
  3. If the deployment is intentionally write-only, stop routing getServices/getOperations calls to this instance.

Example fix

// before
so := core.NewServiceOperationStorage(client, logger, nil /* write-only */)
services, err := so.GetServices(ctx) // errNoSearcher
// after
so := core.NewServiceOperationStorage(client, logger, searcher)
services, err := so.GetServices(ctx)
Defensive patterns

Strategy: validation

Validate before calling

// Before exposing read APIs, confirm the storage was built with a searcher:
if so == nil || !readCapable {
    return errors.New("service/operation reads require a searcher")
}

Try / catch

services, err := soStorage.GetServices(ctx)
if err != nil && strings.Contains(err.Error(), "write-only use") {
    // misconfigured instance: route reads to a read-capable storage
}

Prevention

When it happens

Trigger: Constructing the storage via NewServiceOperationStorage with a nil searcher (write-only use) and then calling getServices or getOperations (exposed through the read API). TestServiceOperationStorage_ReadWithoutSearcher covers exactly this path.

Common situations: Deploying a writer-only storage factory (e.g. remote-storage span-writer roles) and accidentally exposing it to a query component; sharing one storage instance between writer and reader roles when only one was wired with a searcher.

Related errors


AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01). Data as JSON: /api/errors/630b3781c16a8d44. Report an issue: GitHub.