Multi-Tenancy
Associate traces with specific tenants in multi-tenant applications.
What is Multi-Tenancy?
Multi-tenancy allows you to tag traces with a tenant identifier, enabling you to:
- Filter traces by customer or organization
- Track usage and costs per tenant
- Debug issues for specific customers
- Monitor performance across different tenants
- Ensure data isolation in analytics
Setting the Tenant
Use set_tenant() to set the current tenant context:
1234567891011import tracium tracium.init(api_key="sk_live_...") # Set the tenant for the current contexttracium.set_tenant("tenant-123") # All traces now include tenant_id="tenant-123"with tracium.start_trace(agent_name="my-agent") as trace: # This trace is associated with tenant-123 passGetting the Current Tenant
Use get_current_tenant() to retrieve the current tenant:
123456789101112import tracium tracium.set_tenant("tenant-456") # Get the current tenantcurrent = tracium.get_current_tenant()print(current) # "tenant-456" # Returns None if no tenant is settracium.set_tenant(None)current = tracium.get_current_tenant()print(current) # NoneWeb Framework Integration
Set the tenant at the start of each request based on authentication or headers:
FastAPI
123456789101112131415161718192021222324252627282930313233343536import traciumfrom fastapi import FastAPI, Request, Depends tracium.trace()app = FastAPI() def get_tenant_from_request(request: Request) -> str | None: """Extract tenant from JWT, header, or subdomain.""" # From header tenant = request.headers.get("X-Tenant-ID") if tenant: return tenant # From JWT claims (example) # token = request.headers.get("Authorization") # claims = decode_jwt(token) # return claims.get("tenant_id") return None @app.middleware("http")async def tenant_middleware(request: Request, call_next): tenant = get_tenant_from_request(request) if tenant: tracium.set_tenant(tenant) response = await call_next(request) # Clear tenant after request tracium.set_tenant(None) return response @app.post("/chat")async def chat(message: str): # Traces here are associated with the tenant return {"response": "Hello!"}Flask
12345678910111213141516171819202122232425import traciumfrom flask import Flask, request, g tracium.trace()app = Flask(__name__) def get_tenant_from_request() -> str | None: """Extract tenant from request.""" return request.headers.get("X-Tenant-ID") @app.before_requestdef set_tenant_context(): tenant = get_tenant_from_request() if tenant: tracium.set_tenant(tenant) g.tenant_id = tenant @app.teardown_requestdef clear_tenant_context(exception=None): tracium.set_tenant(None) @app.route("/chat", methods=["POST"])def chat(): # Traces here are associated with the tenant return {"response": "Hello!"}Django
1234567891011121314151617181920212223242526# middleware.pyimport tracium class TenantMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Extract tenant from request tenant = request.headers.get("X-Tenant-ID") if tenant: tracium.set_tenant(tenant) response = self.get_response(request) # Clear tenant after request tracium.set_tenant(None) return response # settings.pyMIDDLEWARE = [ # ... other middleware 'your_app.middleware.TenantMiddleware',]Context Propagation
The tenant context automatically propagates through:
- Async/await - Context variables propagate automatically
- Threads - Tracium patches ThreadPoolExecutor for propagation
- Background tasks - Works with Celery and other task queues
123456789101112131415161718192021import traciumimport asyncio tracium.init(api_key="sk_live_...") async def process_request(tenant_id: str): tracium.set_tenant(tenant_id) # Tenant propagates to nested async calls await fetch_data() await generate_response() async def fetch_data(): # tenant_id is available here tenant = tracium.get_current_tenant() print(f"Fetching data for {tenant}") async def generate_response(): # tenant_id is still available tenant = tracium.get_current_tenant() print(f"Generating response for {tenant}")Tenant ID Formats
The tenant ID can be any string up to 255 characters. Common formats include:
# UUID
tracium.set_tenant("550e8400-e29b-41d4-a716-446655440000")
# Slug
tracium.set_tenant("acme-corp")
# Prefixed ID
tracium.set_tenant("org_123456")
tracium.set_tenant("customer_abc123")
# Composite (org + workspace)
tracium.set_tenant("org_123:workspace_456")Best Practices
Set Tenant Early
Set the tenant at the beginning of request handling, before any tracing occurs. Use middleware or decorators for consistency.
Clear Tenant After Requests
Always clear the tenant context after the request completes to prevent tenant leakage between requests.
Use Consistent IDs
Use the same tenant identifier across your system (database IDs, auth tokens, etc.) for easy correlation.
Handle Missing Tenants
Decide how to handle requests without a tenant - either reject them or use a default like "anonymous" or "system".
Complete Example
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import traciumfrom fastapi import FastAPI, Request, HTTPExceptionfrom openai import OpenAI # Initialize Traciumtracium.init( api_key="sk_live_...", default_version="1.0.0",)tracium.trace() app = FastAPI()openai = OpenAI() def extract_tenant(request: Request) -> str: """Extract and validate tenant from request.""" tenant = request.headers.get("X-Tenant-ID") if not tenant: raise HTTPException(status_code=401, detail="Tenant ID required") return tenant @app.middleware("http")async def tenant_middleware(request: Request, call_next): try: tenant = extract_tenant(request) tracium.set_tenant(tenant) except HTTPException: # Allow the route to handle missing tenant pass response = await call_next(request) tracium.set_tenant(None) return response @app.post("/chat")async def chat(message: str, request: Request): tenant = tracium.get_current_tenant() if not tenant: raise HTTPException(status_code=401, detail="Tenant required") # This trace includes tenant_id automatically response = openai.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": message}] ) return { "tenant": tenant, "response": response.choices[0].message.content }