Skip to main content
This example runs a session that exercises four different Mellea operations — m.instruct(), a @generative classifier, a @generative entity extractor, and a multi-turn m.chat() — while OpenTelemetry instrumentation records each step. Two independent trace scopes control what gets recorded: the application trace covers Mellea-level operations, and the backend trace covers raw LLM calls. Source file: docs/examples/telemetry/telemetry_example.py

Concepts covered

  • The two independent trace scopes: mellea.application and mellea.backend
  • Controlling tracing with MELLEA_TRACE_APPLICATION and MELLEA_TRACE_BACKEND environment variables
  • Using start_session() as a context manager so session lifecycle is spanned
  • Exporting spans to an OTLP endpoint (Jaeger)
  • Using mellea.stdlib.requirements.req to attach constraints to m.instruct()

Prerequisites

  • Quick Start complete
  • Ollama running locally with granite4:micro pulled
  • (Optional) Jaeger running locally for span visualisation — see the Jaeger section below
Install with all extras to get the OpenTelemetry dependencies:
uv sync --all-extras

Trace scopes

Mellea defines two independent OpenTelemetry trace scopes.
ScopeEnv varWhat it records
ApplicationMELLEA_TRACE_APPLICATIONSession lifecycle, @generative calls, aact, sampling, requirement validation
BackendMELLEA_TRACE_BACKENDRaw model generation calls, context-based generation, backend-specific operations
Both default to false. Enable either or both independently depending on what you need to observe.

Performance impact

ConfigurationOverhead
Both disabled (default)Near-zero
Application only~1–2 %
Backend only~1–2 %
Both enabled~2–5 %

Running the example

No tracing (baseline)

python docs/examples/telemetry/telemetry_example.py

Application tracing only

export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=false
python docs/examples/telemetry/telemetry_example.py

Backend tracing only

export MELLEA_TRACE_APPLICATION=false
export MELLEA_TRACE_BACKEND=true
python docs/examples/telemetry/telemetry_example.py

Both scopes with console output for debugging

export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=true
export MELLEA_TRACE_CONSOLE=true
python docs/examples/telemetry/telemetry_example.py

Export to an OTLP endpoint (Jaeger)

export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
python docs/examples/telemetry/telemetry_example.py

Starting Jaeger

Run Jaeger in Docker to receive and visualise spans:
docker run -d --name jaeger \
  -p 4317:4317 \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest
After running the example, open http://localhost:16686, select the mellea-example service, and browse the trace timeline.

The full example

Generative function declarations

from mellea import generative, start_session
from mellea.stdlib.requirements import req


@generative
def classify_sentiment(text: str) -> str:
    """Classify the sentiment of the given text as positive, negative, or neutral."""


@generative
def extract_entities(text: str) -> list[str]:
    """Extract named entities from the text."""
These two functions are declared at module level. @generative wires them up to the runtime; no implementation is needed. Each call site below passes a session m as the first argument, which binds the call to the current trace context.

Session as a context manager and introspection

def main():
    """Run example with telemetry instrumentation."""
    print("=" * 60)
    print("Mellea OpenTelemetry Example")
    print("=" * 60)

    # Check which traces are enabled
    from mellea.telemetry import (
        is_application_tracing_enabled,
        is_backend_tracing_enabled,
    )

    print(f"Application tracing: {is_application_tracing_enabled()}")
    print(f"Backend tracing: {is_backend_tracing_enabled()}")
    print("=" * 60)
is_application_tracing_enabled() and is_backend_tracing_enabled() reflect the current environment variable state at runtime. Use these guards in your own code when you want to conditionally add tracing context (for example, adding custom span attributes only when tracing is on).

Operation 1: instruct with requirements

    # Start a session - this will be traced if application tracing is enabled
    with start_session() as m:
        # Example 1: Simple instruction with requirements
        print("\n1. Simple instruction with requirements...")
        email = m.instruct(
            "Write a professional email to {{name}} about {{topic}}",
            requirements=[req("Must be formal"), req("Must be under 100 words")],
            user_variables={"name": "Alice", "topic": "project update"},
        )
        print(f"Generated email: {str(email)[:100]}...")
Using start_session() as a context manager (with start_session() as m:) means the session open and close events are recorded as the root span when application tracing is enabled. All child operations appear nested under this root. req("Must be formal") attaches a soft requirement to the generation. Requirements appear as span attributes in the trace so you can see which constraints were applied and whether they triggered a retry.

Operation 2: @generative sentiment classifier

        # Example 2: Using @generative function
        print("\n2. Using @generative function...")
        sentiment = classify_sentiment(
            m, text="I absolutely love this product! It's amazing!"
        )
        print(f"Sentiment: {sentiment}")
Each @generative call produces its own child span in the application trace. The span includes the function name, parameter names, and the inferred return type.

Operation 3: @generative entity extractor

        # Example 3: Multiple operations
        print("\n3. Multiple operations...")
        text = "Apple Inc. announced new products in Cupertino, California."
        entities = extract_entities(m, text=text)
        print(f"Entities: {entities}")
Running multiple @generative calls inside the same with block keeps them all under the same root span. In Jaeger you can see the sequence and duration of each call on a single timeline.

Operation 4: multi-turn chat

        # Example 4: Chat interaction
        print("\n4. Chat interaction...")
        response1 = m.chat("What is 2+2?")
        print(f"Response 1: {response1!s}")

        response2 = m.chat("Multiply that by 3")
        print(f"Response 2: {response2!s}")
m.chat() is a stateful multi-turn method. The session accumulates turn history, so response2 can refer back to the result of response1 without repeating the context. Both turns appear as sibling spans under the root session span.

Full file

# pytest: ollama, llm

"""Example demonstrating OpenTelemetry tracing in Mellea.

This example shows how to use the two independent trace scopes:
1. Application trace - tracks user-facing operations
2. Backend trace - tracks LLM backend interactions

Run with different configurations:

# Enable only application tracing
export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=false
python telemetry_example.py

# Enable only backend tracing
export MELLEA_TRACE_APPLICATION=false
export MELLEA_TRACE_BACKEND=true
python telemetry_example.py

# Enable both traces
export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=true
python telemetry_example.py

# Export to OTLP endpoint (e.g., Jaeger)
export MELLEA_TRACE_APPLICATION=true
export MELLEA_TRACE_BACKEND=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
python telemetry_example.py

# Enable console output for debugging
export MELLEA_TRACE_CONSOLE=true
python telemetry_example.py
"""

from mellea import generative, start_session
from mellea.stdlib.requirements import req


@generative
def classify_sentiment(text: str) -> str:
    """Classify the sentiment of the given text as positive, negative, or neutral."""


@generative
def extract_entities(text: str) -> list[str]:
    """Extract named entities from the text."""


def main():
    """Run example with telemetry instrumentation."""
    print("=" * 60)
    print("Mellea OpenTelemetry Example")
    print("=" * 60)

    # Check which traces are enabled
    from mellea.telemetry import (
        is_application_tracing_enabled,
        is_backend_tracing_enabled,
    )

    print(f"Application tracing: {is_application_tracing_enabled()}")
    print(f"Backend tracing: {is_backend_tracing_enabled()}")
    print("=" * 60)

    # Start a session - this will be traced if application tracing is enabled
    with start_session() as m:
        # Example 1: Simple instruction with requirements
        print("\n1. Simple instruction with requirements...")
        email = m.instruct(
            "Write a professional email to {{name}} about {{topic}}",
            requirements=[req("Must be formal"), req("Must be under 100 words")],
            user_variables={"name": "Alice", "topic": "project update"},
        )
        print(f"Generated email: {str(email)[:100]}...")

        # Example 2: Using @generative function
        print("\n2. Using @generative function...")
        sentiment = classify_sentiment(
            m, text="I absolutely love this product! It's amazing!"
        )
        print(f"Sentiment: {sentiment}")

        # Example 3: Multiple operations
        print("\n3. Multiple operations...")
        text = "Apple Inc. announced new products in Cupertino, California."
        entities = extract_entities(m, text=text)
        print(f"Entities: {entities}")

        # Example 4: Chat interaction
        print("\n4. Chat interaction...")
        response1 = m.chat("What is 2+2?")
        print(f"Response 1: {response1!s}")

        response2 = m.chat("Multiply that by 3")
        print(f"Response 2: {response2!s}")

    print("\n" + "=" * 60)
    print("Example complete!")
    print("=" * 60)
    print("\nTrace data has been exported based on your configuration.")
    print("If OTEL_EXPORTER_OTLP_ENDPOINT is set, check your trace backend.")
    print("If MELLEA_TRACE_CONSOLE=true, traces are printed above.")


if __name__ == "__main__":
    main()

Span attributes

Each span in the application trace includes the following attributes where applicable:
AttributeDescription
model_idModel identifier used for the call
backendBackend class name (e.g. OllamaBackend)
action_typeComponent type (e.g. generative, instruct)
context_sizeNumber of context items passed
has_requirementsWhether requirements were specified
strategy_typeSampling strategy used
tool_callsWhether tool calling was enabled
format_typeResponse format class

What to try next

  • Set OTEL_SERVICE_NAME=my-app to customise the service name in your trace backend.
  • See OpenTelemetry Tracing for attribute schemas and advanced configuration.
  • Add MELLEA_TRACE_CONSOLE=true alongside an OTLP endpoint to confirm spans are generated even when the remote collector is unavailable.