Skip to main content
This guide describes how to send OpenTelemetry traces to the Unomiq OTel Gateway API.

Prerequisites

API Credentials

Create API credentials from the Unomiq Dashboard. The credentials must have the write:traces permission. This will give you an API key (Client ID) and secret (Client Secret).

With unomiq-sdk

The unomiq-sdk Python package handles OAuth2-authenticated OTLP export and unit attribute attachment. It can be used on its own (direct export) or alongside an OTel Collector sidecar.

Configuration

Set the following environment variables for your application:
VariableDescription
UNOMIQ_CLIENT_IDAPI Key from the Unomiq Management API / dashboard
UNOMIQ_CLIENT_SECRETAPI Secret from the Unomiq Management API / dashboard
The SDK uses these credentials internally to acquire and refresh OAuth2 tokens and export traces to the Unomiq Gateway.

Direct Export (No Sidecar)

In this approach, unomiq-sdk handles everything: it creates a TracerProvider, configures an OAuth2-authenticated OTLP exporter, and attaches unit attributes to spans.
Your App ──(OAuth2 Bearer token)──▶ Gateway
This is useful for serverless environments (Cloud Run, Lambda) or when you want fewer infrastructure components.
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from unomiq.sdk import Unomiq


def configure_tracing(service_name: str, service_version: str) -> trace.Tracer:
    """Set up tracing with Unomiq SDK (handles OAuth and export)."""

    resource = Resource.create({
        SERVICE_NAME: service_name,
        SERVICE_VERSION: service_version,
    })

    Unomiq.init(
        app_name=service_name,
        resolve_unit=lambda: "my-unit-id",
        resolve_parent_unit=lambda: "my-org-id",
        resource_attributes=dict(resource.attributes),
    )

    return trace.get_tracer(service_name, service_version)
With this setup, the SDK reads UNOMIQ_CLIENT_ID and UNOMIQ_CLIENT_SECRET from the environment, creates an authenticated OTLP exporter, and sets up the global TracerProvider. No additional exporter or provider configuration is needed. For frameworks like Flask or Django, you can use OpenTelemetry instrumentor libraries after calling Unomiq.init():
from opentelemetry.instrumentation.flask import FlaskInstrumentor

tracer = configure_tracing("my-service", "1.0.0")
FlaskInstrumentor().instrument_app(app)

With OTel Collector Sidecar

You can also use unomiq-sdk alongside a Collector sidecar. In this case, the sidecar handles OAuth and export, while unomiq-sdk only handles unit attribute attachment.
Your App ──(no auth)──▶ OTel Collector Sidecar ──(OAuth2 Bearer token)──▶ Gateway
from opentelemetry import trace
from unomiq.sdk import Unomiq

# The TracerProvider is already created by opentelemetry-instrument or your
# own setup. Unomiq.init() only adds the UnitSpanProcessor.
Unomiq.init(
    resolve_unit=lambda: "my-unit-id",
    resolve_parent_unit=lambda: "my-org-id",
)

tracer = trace.get_tracer(__name__)
Since auth and export are handled by the collector, the SDK only needs resolve_unit / resolve_parent_unit. No app_name or resource_attributes are required. Point your application’s OTLP exporter at the local collector:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
See the OTel Collector Sidecar section below for the collector configuration.

Without unomiq-sdk

If you are not using unomiq-sdk, you can send traces by managing the OAuth2 flow and OTLP export yourself. There are two approaches:
  1. Collector Sidecar — an OTel Collector runs alongside your app, handles OAuth2, and forwards traces to the gateway.
  2. Direct from Application — your application code manages OAuth2 tokens and sends traces to the gateway.
Both approaches use the OAuth2 client credentials grant type, with automatic refresh token support when available.

Configuration

Set the following environment variables:
VariableDescription
UNOMIQ_CLIENT_IDAPI Key from the Unomiq dashboard
UNOMIQ_CLIENT_SECRETAPI Secret from the Unomiq dashboard
UNOMIQ_TOKEN_URLhttps://oauth-api.unomiq.com/token
UNOMIQ_AUDIENCEhttps://gateway-api.unomiq.com
UNOMIQ_OTLP_ENDPOINThttps://gateway-api.unomiq.com

Direct from Application (Python)

In this approach, your application manages OAuth2 tokens and sends authenticated traces directly to the gateway. No sidecar is needed.
Your App ──(OAuth2 Bearer token)──▶ Gateway

Step 1: OAuth2 Token Manager

Create a token manager that handles the client credentials flow and automatic refresh. If the token endpoint returns a refresh_token, subsequent renewals use the lighter refresh_token grant instead of re-sending client credentials:
"""OAuth2 token manager for OTLP exporters."""

import threading
import logging
from typing import Optional
from datetime import datetime, timedelta

import requests

logger = logging.getLogger(__name__)


class OAuthTokenManager:
    """Manages OAuth2 token retrieval and automatic refresh."""

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        token_url: str,
        scopes: str,
        audience: str,
        refresh_buffer: int = 300,  # Refresh 5 minutes before expiry
    ):
        self.client_id = client_id
        self.client_secret = client_secret
        self.token_url = token_url
        self.scopes = scopes
        self.audience = audience
        self.refresh_buffer = refresh_buffer

        self._token: Optional[str] = None
        self._refresh_token: Optional[str] = None
        self._token_expiry: Optional[datetime] = None
        self._lock = threading.Lock()
        self._stop_refresh = threading.Event()

        # Get initial token
        self._fetch_token()

        # Start background refresh
        self._refresh_thread = threading.Thread(
            target=self._refresh_loop, daemon=True, name="oauth-token-refresh"
        )
        self._refresh_thread.start()

    def _fetch_token(self) -> None:
        """Fetch a new access token using client credentials."""
        response = requests.post(
            self.token_url,
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "scope": self.scopes,
                "audience": self.audience,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=10,
        )
        response.raise_for_status()

        token_data = response.json()
        self._token = token_data["access_token"]
        self._refresh_token = token_data.get("refresh_token")
        expires_in = token_data.get("expires_in", 3600)
        self._token_expiry = datetime.now() + timedelta(seconds=expires_in)
        logger.info("OAuth token fetched, expires in %d seconds", expires_in)

    def _refresh_token_grant(self) -> None:
        """Use the refresh token to obtain a new access token."""
        try:
            response = requests.post(
                self.token_url,
                data={
                    "grant_type": "refresh_token",
                    "client_id": self.client_id,
                    "refresh_token": self._refresh_token,
                    "audience": self.audience,
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                timeout=10,
            )
            response.raise_for_status()

            token_data = response.json()
            self._token = token_data["access_token"]
            # Support refresh token rotation
            if "refresh_token" in token_data:
                self._refresh_token = token_data["refresh_token"]
            expires_in = token_data.get("expires_in", 3600)
            self._token_expiry = datetime.now() + timedelta(seconds=expires_in)
            logger.info("OAuth token refreshed via refresh_token, expires in %d seconds", expires_in)

        except Exception as e:
            logger.warning("Refresh token grant failed: %s, falling back to client_credentials", e)
            self._refresh_token = None
            self._fetch_token()

    def _should_refresh(self) -> bool:
        if self._token is None or self._token_expiry is None:
            return True
        return (self._token_expiry - datetime.now()).total_seconds() <= self.refresh_buffer

    def _refresh_loop(self) -> None:
        while not self._stop_refresh.is_set():
            try:
                if self._should_refresh():
                    with self._lock:
                        if self._should_refresh():
                            if self._refresh_token:
                                self._refresh_token_grant()
                            else:
                                self._fetch_token()
            except Exception:
                logger.exception("Error refreshing OAuth token")
            self._stop_refresh.wait(60)

    def get_headers(self) -> dict:
        """Return authorization headers for the OTLP exporter."""
        with self._lock:
            if self._should_refresh():
                if self._refresh_token:
                    self._refresh_token_grant()
                else:
                    self._fetch_token()
            return {"Authorization": f"Bearer {self._token}"}

    def stop(self) -> None:
        """Stop the background refresh thread."""
        self._stop_refresh.set()

Step 2: Configure the Tracer

Wire the token manager into the OTLP exporter:
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from oauth_token_manager import OAuthTokenManager


def configure_tracing(service_name: str, service_version: str) -> trace.Tracer:
    """Set up OpenTelemetry tracing with OAuth2-authenticated export."""

    # Create the token manager
    token_manager = OAuthTokenManager(
        client_id=os.environ["UNOMIQ_CLIENT_ID"],
        client_secret=os.environ["UNOMIQ_CLIENT_SECRET"],
        token_url=os.environ["UNOMIQ_TOKEN_URL"],
        scopes="write:traces",
        audience=os.environ["UNOMIQ_AUDIENCE"],
    )

    # Build the OTLP exporter with auth headers
    endpoint = os.environ["UNOMIQ_OTLP_ENDPOINT"].rstrip("/") + "/v1/traces"
    exporter = OTLPSpanExporter(
        endpoint=endpoint,
        headers=token_manager.get_headers(),
    )

    # Assemble the tracer provider
    resource = Resource.create({
        SERVICE_NAME: service_name,
        SERVICE_VERSION: service_version,
    })
    provider = TracerProvider(resource=resource)
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)

    return trace.get_tracer(service_name, service_version)

Step 3: Use the Tracer

tracer = configure_tracing("my-service", "1.0.0")

with tracer.start_as_current_span("my-operation") as span:
    span.set_attribute("key", "value")
    # ... your code here
For frameworks like Flask or Django, you can also use the OpenTelemetry instrumentor libraries (e.g., opentelemetry-instrumentation-flask) after calling configure_tracing().

Required Dependencies

opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http
requests

OTel Collector Sidecar Configuration

This section applies to both the unomiq-sdk sidecar approach and the without-SDK sidecar approach. In both cases, the collector handles OAuth2 token acquisition and forwards authenticated traces to the gateway.
Your App ──(no auth)──▶ OTel Collector Sidecar ──(OAuth2 Bearer token)──▶ Gateway
The collector uses the oauth2clientauthextension from the OpenTelemetry Collector Contrib distribution.

Environment Variables

Set the following environment variables for the collector (not the application):
VariableDescription
UNOMIQ_CLIENT_IDAPI Key from the Unomiq Management API / dashboard
UNOMIQ_CLIENT_SECRETAPI Secret from the Unomiq Management API / dashboard
UNOMIQ_TOKEN_URLhttps://oauth-api.unomiq.com/token
UNOMIQ_AUDIENCEhttps://gateway-api.unomiq.com
UNOMIQ_OTLP_ENDPOINThttps://gateway-api.unomiq.com

Collector Config

Create an otel-collector-config.yaml:
extensions:
  oauth2client:
    client_id: ${env:UNOMIQ_CLIENT_ID}
    client_secret: ${env:UNOMIQ_CLIENT_SECRET}
    token_url: ${env:UNOMIQ_TOKEN_URL}
    endpoint_params:
      audience: ${env:UNOMIQ_AUDIENCE}
    scopes:
      - write:traces
    timeout: 10s

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 100

exporters:
  otlphttp:
    endpoint: ${env:UNOMIQ_OTLP_ENDPOINT}
    auth:
      authenticator: oauth2client

service:
  extensions: [oauth2client]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
Key points:
  • The oauth2client extension acquires a token using the client credentials grant and automatically refreshes it before expiry.
  • The otlphttp exporter references the extension via auth.authenticator, so every outgoing request includes the Authorization: Bearer <token> header.
  • The receiver listens on standard OTLP ports (4317 for gRPC, 4318 for HTTP) without requiring any authentication from your application.

Running the Collector

Use the contrib distribution of the collector, which includes the oauth2clientauthextension. The core distribution does not include it. Docker Compose example:
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    command: ["--config=/etc/otelcol/config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol/config.yaml
    environment:
      - UNOMIQ_CLIENT_ID
      - UNOMIQ_CLIENT_SECRET
      - UNOMIQ_TOKEN_URL
      - UNOMIQ_AUDIENCE
      - UNOMIQ_OTLP_ENDPOINT
    ports:
      - "4317:4317"
      - "4318:4318"

Configuring Your Application

Point your application’s OTLP exporter at the local collector. No authentication configuration is needed in the application itself. Python (automatic instrumentation):
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
opentelemetry-instrument python app.py
Python (SDK):
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
Any language: Set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to http://localhost:4318 (or the collector’s hostname in Docker/Kubernetes).

Choosing an Approach

unomiq-sdk (Direct)unomiq-sdk (Sidecar)Without SDK (Sidecar)Without SDK (Direct)
Auth handlingSDK manages tokensCollector manages tokensCollector manages tokensApplication manages tokens
Unit attachmentAutomatic via SDKAutomatic via SDKManual (see guide)Manual (see guide)
Code changesMinimalMinimalNone (env vars only)Token manager + exporter setup
InfrastructureNo extra infrastructureRequires running a collectorRequires running a collectorNo extra infrastructure
Language supportPythonPython + any (collector is language-agnostic)Any (language-agnostic)Requires per-language implementation
Best forServerless (Cloud Run, Lambda)Docker, KubernetesDocker, Kubernetes (non-Python)Serverless (non-Python or without SDK)

Monitoring Sent Traces

Once your traces are flowing to the gateway, you can monitor them in two ways:
  • API — Use the Get Live Traces endpoint to programmatically retrieve and inspect incoming traces in real time.
  • Dashboard — View and explore traces visually from the Unomiq Dashboard.

Troubleshooting

Common Issues

401 Unauthorized from the gateway
  • Verify that your client ID and secret are correct (UNOMIQ_CLIENT_ID / UNOMIQ_CLIENT_SECRET).
  • Check that the scopes match what the gateway expects (write:traces).
  • Ensure the token endpoint is reachable from your environment.
No traces appearing at the gateway
  • Confirm the OTLP endpoint is set to the correct gateway URL.
  • For the sidecar approach, check collector logs for export errors.
  • For direct export, enable debug logging: logging.getLogger('opentelemetry').setLevel(logging.DEBUG).
Token refresh failures
  • The sidecar collector and the unomiq-sdk / Python token manager all refresh tokens automatically before expiry. Check logs for errors from the token endpoint.
  • Ensure your OAuth2 client has not been revoked or rate-limited.

Viewing Collector Logs (Sidecar Approach)

Add the debug exporter to your collector pipeline for verbose output:
exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      exporters: [otlphttp, debug]