{"record":{"id":"630b3781c16a8d44","repo":"jaegertracing/jaeger","slug":"service-operation-reads-require-a-searcher-but-th","errorCode":null,"errorMessage":"service/operation reads require a searcher, but this storage was constructed for write-only use","messagePattern":"service/operation reads require a searcher, but this storage was constructed for write-only use","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/v2/elasticsearch/tracestore/core/service_operation.go","lineNumber":38,"sourceCode":"\t\"github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/tracestore/core/dbmodel\"\n)\n\nconst (\n\tserviceName = \"serviceName\"\n\n\t// serviceCacheTTLDefault is how long a written service:operation pair is\n\t// remembered when the configuration does not say.\n\tserviceCacheTTLDefault = 12 * time.Hour\n\n\toperationsAggregation = \"distinct_operations\"\n\tservicesAggregation   = \"distinct_services\"\n)\n\n// errNoSearcher is returned by the read methods (getServices/getOperations) when\n// the storage was constructed without a searcher — the write-only case described\n// on NewServiceOperationStorage. It turns that misconfiguration into a clear\n// error rather than a nil-pointer panic.\nvar errNoSearcher = errors.New(\"service/operation reads require a searcher, but this storage was constructed for write-only use\")\n\n// ServiceOperationStorage stores service to operation pairs.\ntype ServiceOperationStorage struct {\n\tsearcher     esclient.Searcher\n\tlogger       *zap.Logger\n\tserviceCache cache.Cache\n}\n\n// NewServiceOperationStorage returns a new ServiceOperationStorage. searcher is\n// used only by the read methods (getServices/getOperations); a write-only instance\n// (the SpanWriter) may pass a nil searcher. The write side builds documents via\n// toUpsertItem and commits the cache via commitToCache — it does not write directly.\n// A cacheTTL of zero selects serviceCacheTTLDefault.\nfunc NewServiceOperationStorage(\n\tsearcher esclient.Searcher,\n\tlogger *zap.Logger,\n\tcacheTTL time.Duration,\n) *ServiceOperationStorage {","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/jaegertracing/jaeger/blob/806f4447841ecdb60519f408b004a599d515f437/internal/storage/v2/elasticsearch/tracestore/core/service_operation.go#L20-L56","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Construct the storage with a non-nil searcher if you need service/operation reads.","Use a separate, read-capable storage instance for the query path.","If the deployment is intentionally write-only, stop routing getServices/getOperations calls to this instance."],"exampleFix":"// before\nso := core.NewServiceOperationStorage(client, logger, nil /* write-only */)\nservices, err := so.GetServices(ctx) // errNoSearcher\n// after\nso := core.NewServiceOperationStorage(client, logger, searcher)\nservices, err := so.GetServices(ctx)","handlingStrategy":"validation","validationCode":"// Before exposing read APIs, confirm the storage was built with a searcher:\nif so == nil || !readCapable {\n    return errors.New(\"service/operation reads require a searcher\")\n}","typeGuard":null,"tryCatchPattern":"services, err := soStorage.GetServices(ctx)\nif err != nil && strings.Contains(err.Error(), \"write-only use\") {\n    // misconfigured instance: route reads to a read-capable storage\n}","preventionTips":["Match storage construction to role: writers get a nil searcher, readers must supply one.","Keep separate instances for read and write roles instead of sharing one.","Assert searcher non-nil in tests for any component that exposes read endpoints."],"tags":["elasticsearch","configuration","write-only-storage"],"backgroundTag":"storage-not-configured-for-reads","analyzedSha":"806f4447841ecdb60519f408b004a599d515f437","analyzedAt":"2026-09-01T02:39:22.140Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}