Opper’s tracing endpoints allow for customizing logging of task completions into hierarchical views. It also allows for appending metrics and metadata to logs so that teams can observe behavior of models and task completions during development or in production.

How it works

Tracing works by creating and updating spans. Spans can have parents and children, which means that they can be connected to each other. By default, all task completions or knowledge base operations are automatically attached to a span which will have the name of the task. In the return values of operations the span_id is listed. All operations have a parent_span_id which can be used to connect the operation to a span.

Creating new spans

Task completions can be attached to spans. Here is a simple example where we create a root span named “data_processing”:

from opperai import Opper
import os

def main():
    # Initialize Opper client
    opper = Opper(http_bearer=os.getenv("OPPER_API_KEY"))

    # Create a trace to track this processing session
    session_span = opper.spans.create(name="data_processing")

    # Sample data to process
    sample_data = [
        {"name": "Alice", "age": 30, "city": "New York"},
        {"name": "Bob", "age": 25, "city": "San Francisco"},
        {"name": "Charlie", "age": 35, "city": "Chicago"},
        {"name": "Diana", "age": 28, "city": "Boston"},
    ]

    personas = []
    for i, record in enumerate(sample_data):

        # Analyze the record and connect it to the trace
        completion = opper.call(
            name="analyze",
            instructions="Analyze this person's data and provide insights about their profile.",
            input=record,
            parent_span_id=session_span.id,
        )

        analysis = completion.message
        personas.append(analysis)

        print(f"Analysis: {analysis}")
    
if __name__ == "__main__":
    main() 
It is possible to create flexible hierarchies of spans to capture the run of an agent or chatbot in the best way possible. It is often good to think about a span as a container of some kind of higher level operation. For example, for a chatbot you may want a root span to be a session, each round of the chat a round, and then have all underlying task completions inside the round span.

Setting span input and output fields

Span input and output fields can be useful to surface information.

Here we update the root span with information on what will be processed and the final output:

opper.spans.update(
    span_id=session_span.id, 
    input=str(sample_data),
    output=str(personas),
    meta={"n_records": len(sample_data)}
)
Since it’s possible to choose exactly what to display in the input and output fields of a span it is a good way to render information for more easy digestion, maybe for non-technical experts and other peers

Adding metrics to spans

It is possible to add metrics to spans, to catch results of evaluations or performance measurements. Metrics can be filtered for in the tracing view in the dashboard.

Here is a simple example :

# Save a metric that captures number of personas that are blank
# and attach it to the root span
opper.span_metrics.create_metric(
    span_id=session_span.id,
    dimension="n_failed",
    value=sum(1 for persona in personas if persona is None),
    comment="Number of personas with failed summary",
)
We recommend thinking about metrics as a way to store various performance evaluations of the feature you are building. Metrics can hold things like thumbs up/down, results of tests from requirements and so forth.