Skip to content

Multitenancy: is the same Trace ID across tenants supported? Trace-by-ID returns 404 for one tenant #255

Description

@MuNeNiCK

Is your question related to a specific component?

VictoriaTraces single-node v0.11.0, trace ID indexing and Jaeger trace-by-ID API.

Describe the question in detail

Does VictoriaTraces support storing spans with the same Trace ID in different tenants, with each tenant querying only its own spans?

For example, two applications belonging to different tenants may propagate the same trace context during an inter-service request and export their respective spans to their own tenants. We are not asking for cross-tenant queries or access to another tenant's spans.

The documentation describes multitenancy via AccountID/ProjectID headers. In an isolated reproduction using the official v0.11.0 image, two tenants can store spans with the same Trace ID and different Span IDs. LogsQL returns each tenant's own span, but Jaeger trace-by-ID returns HTTP 200 for the first tenant and HTTP 404 for the second.

Questions:

  1. Is this use case supported?
  2. Is the behavior below a bug or an intentional limitation?
  3. Is there a design reason why the in-memory trace index key excludes the tenant ID? If this is intentional, what configuration or usage pattern is recommended, and where is the limitation documented?

Reproduction

Start a fresh instance (no mounted data). The shortened index flush interval makes the example faster; the original observation also occurred with the default interval.

docker run --rm --name vt-tenant-repro -p 127.0.0.1:19429:10428   victoriametrics/victoria-traces:v0.11.0   -storageDataPath=/tmp/traces -insert.indexFlushInterval=2s

Run this Python 3 script in another terminal. It uses only the standard library, synthetic data, different Span IDs, and timestamps two minutes in the past. It waits 15 seconds after ingestion, longer than the configured index buffering and default data flush intervals.

import json, time, urllib.request, urllib.parse, urllib.error
BASE = 'http://127.0.0.1:19429'
TRACE = '1234567890abcdef1234567890abcdef'
def request(path, account, data=None):
    headers = {'AccountID': str(account), 'ProjectID': '7'}
    if data is not None:
        headers['Content-Type'] = 'application/json'
        data = json.dumps(data).encode()
    try:
        with urllib.request.urlopen(urllib.request.Request(BASE + path, data=data, headers=headers), timeout=15) as r:
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()
start = time.time_ns() - 120_000_000_000
for account, span in [(101, '1111111111111111'), (102, '2222222222222222')]:
    payload = {'resourceSpans': [{'resource': {'attributes': [{'key': 'service.name', 'value': {'stringValue': 'service-' + str(account)}}]}, 'scopeSpans': [{'spans': [{'traceId': TRACE, 'spanId': span, 'name': 'example', 'kind': 2, 'startTimeUnixNano': str(start), 'endTimeUnixNano': str(start + 100_000_000)}]}]}]}
    print('INSERT', account, request('/insert/opentelemetry/v1/traces', account, payload), flush=True)
# Wait longer than both index buffers and the default data flush interval.
time.sleep(15)
for account in [101, 102]:
    query = urllib.parse.urlencode({'query': 'trace_id:="' + TRACE + '"', 'limit': '10'})
    print('RAW', account, request('/select/logsql/query?' + query, account), flush=True)
    print('JAEGER', account, request('/select/jaeger/api/traces/' + TRACE, account), flush=True)

Observed:

Tenant (AccountID, ProjectID) OTLP insert LogsQL span query Jaeger trace-by-ID
(101, 7) 200 own span 1111111111111111 200, own span
(102, 7) 200 own span 2222222222222222 404, trace not found

If supported, the expected behavior is that both tenants can retrieve their own spans independently, even when the Trace ID is identical. No cross-tenant span disclosure was observed in this reproduction.

Code investigation

In v0.11.0 index_helper.go, mustPushIndex keys the maps only by the Trace ID. An existing entry updates its time range and returns without comparing tenant IDs. The tenant stored in the entry remains that of the initial insertion. flushIndexInMap then writes the index to that tenant.

This appears to explain the observation, but we would appreciate clarification of the intended multitenancy contract. We reviewed #21, #71 and #81; they discuss indexing performance and accuracy, but we did not find discussion of this cross-tenant case. #109 concerns a recent-trace indexing delay; this reproduction waits beyond the shortened index flush interval.

Documentation consulted

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions