I spent three days trying to figure out why my multi-agent LLM workflow was taking 15 minutes when it should take 5. Without observability, I was stuck adding print statements and rerunning the entire pipeline. That's when I decided to properly instrument it with OpenTelemetry and SigNoz.
This post walks through how I added traces, logs, and metrics to a Flask app that orchestrates 8 LLM agents using Ollama and Mistral. More importantly, it covers what I learned about monitoring agent-based systems that you won't find in the standard OpenTelemetry docs.
The Problem: A Black Box LLM Pipeline
My user story automation app processes requirements documents through 8 specialized agents:
- RequirementsAgent extracts functionalities
- EpicExtractorAgent creates epics in batches
- EpicRefinerAgent polishes the output
- StoryGeneratorAgent writes user stories
- TestCaseAgent generates test scenarios
The workflow runs for 10-15 minutes per document, making debug cycles painful. When something went wrong, I had no idea which agent was slow or failing. I needed to see inside the pipeline.
Setting Up OpenTelemetry: The Basics Plus Agent Tracing
The standard Flask instrumentation gives you HTTP endpoints and database queries, but it doesn't know anything about your agents. Here's what I added to src/backend/otel_config.py:
def init_telemetry(app):
SIGNOZ_BASE = os.getenv('SIGNOZ_OTLP_ENDPOINT', 'http://localhost:4318')
resource = Resource.create({
"service.name": "user-story-automation",
"service.version": "1.0.0",
"deployment.environment": "development",
})
# Traces: Track agent operations and workflow execution
trace_provider = TracerProvider(resource=resource)
trace_exporter = OTLPSpanExporter(endpoint=f"{SIGNOZ_BASE}/v1/traces")
trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
trace.set_tracer_provider(trace_provider)
# Logs: Centralized logging with context
logger_provider = LoggerProvider(resource=resource)
log_exporter = OTLPLogExporter(endpoint=f"{SIGNOZ_BASE}/v1/logs")
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
set_logger_provider(logger_provider)
# Metrics: Custom histograms for agent performance
metric_exporter = OTLPMetricExporter(endpoint=f"{SIGNOZ_BASE}/v1/metrics")
meter_provider = MeterProvider(
resource=resource,
metric_readers=[PeriodicExportingMetricReader(metric_exporter)]
)
metrics.set_meter_provider(meter_provider)
# Auto-instrumentation
FlaskInstrumentor().instrument_app(app)
SQLAlchemyInstrumentor().instrument()
RequestsInstrumentor().instrument()
The auto-instrumentation got me basic traces, but here's the first gotcha: Flask traces showed me /process-document taking 674 seconds, but that single span told me nothing about which agent was slow. I needed manual spans.
Manual Spans: The Only Way to See Agent Operations
I wrapped each agent call with a manual span in src/backend/routes/api.py:
# Requirements extraction
with create_manual_span("agent.requirements_extraction", {
"agent.name": "RequirementsAgent",
"document.length": len(extracted_text)
}):
requirements = rat(refine_doc, extract_functionarity, extracted_text, chat, mode)
# Epic extraction with batch tracking
for batch_idx, batch in enumerate(batches):
with create_manual_span("agent.epic_extraction_batch", {
"agent.name": "EpicExtractorAgent",
"batch.index": batch_idx + 1,
"batch.total": len(batches),
"batch.size": len(batch)
}):
batch_result = extract_epics_func(batch_text, chat, mode)
This is where I learned something important: batch operations need special handling. Without tracking batch.index and batch.total, I couldn't tell if batch 2 was consistently slower than batches 1 and 3 (spoiler: it was).
What The Data Revealed
Once the instrumentation was running, I built dashboards using SigNoz's MCP integration. Here's what I actually learned from the data.
Finding the Bottleneck
The main dashboard showed average workflow time at 13.28 minutes across 3 test runs. Breaking it down by agent revealed the issue:

- Epic Extraction Batch: 11 operations, 2.06 minutes average
- Requirements Extraction: 3 operations, 2.68 minutes average
- Epic Refinement: 2 operations, 5.77 minutes average
Epic Refinement was taking 3x longer per operation than anything else. That's where I needed to optimize.
The pie chart showed epic extraction batch consuming 48% of total operations, but the table showed refinement had the worst P95 latency at 7.49 minutes.
P99 vs P50: Why Percentiles Matter
I added a Performance Bottlenecks dashboard to track percentile latency. This is where things got interesting:
The graph shows P99 latency spiking to 16.67 minutes while P50 stayed around 3.33 minutes. That's a 5x difference. This told me the LLM (Ollama running Mistral locally) had massive variance in response time, probably due to resource contention.
The time distribution pie chart revealed another insight: epic extraction batch consumes 45% (22.83 mins) of total workflow time across all agents. Even though individual operations are fast (2.06 min average), the sheer volume makes it the biggest time sink.
Error Tracking by Agent
I built an Advanced Analytics dashboard with per-agent error rates:
The error rate chart showed epic_refinement had a 4% error spike on July 17th, then recovered. Without this view, I would never have known there was a transient issue that resolved itself.
The success rate panel showed 100% overall, but the multi-percentile latency breakdown (P50/P90/P95/P99 on one chart) made it obvious where the variance was coming from.
Using SigNoz MCP for Dashboard Creation
Instead of clicking through the UI, I used SigNoz's Model Context Protocol (MCP) server with Claude. I configured it in ~/.claude.json:
{
"mcp": {
"signoz": {
"url": "http://localhost:8000/mcp",
"headers": {
"SIGNOZ-API-KEY": "your-api-key-here"
}
}
}
}
Then I could create dashboards by describing what I wanted:
Me: "Create a dashboard showing agent performance with P95 latency"
Assistant: *Uses signoz_create_dashboard MCP tool*
Result: Complete dashboard with 10 widgets created in seconds
This was 10x faster than clicking through the UI. I created all 5 dashboards this way, including complex multi-query widgets for the Advanced Analytics dashboard.
Alerts: Catching Issues Before They Matter
I set up 7 alerts total, but two were particularly useful:
1. Agent Performance Anomaly Detection
This uses z-score anomaly detection (2 standard deviations) on the agent_duration_seconds metric with hourly seasonality. It caught a case where epic refinement suddenly took 2x normal time due to LLM resource contention. A static threshold would have missed this gradual degradation.
2. Workflow Completion Rate
I track agent.requirements_extraction (the first operation in every workflow) and calculate success rate with a formula query:
(successful_operations / total_operations) * 100
The alert fires if completion rate drops below 90%. This was tricky to set up because I initially filtered for /process-document span name, which doesn't exist. I had to search traces to find the actual span names being generated.
What I Learned (The Stuff Not In The Docs)
1. Manual Spans Are Non-Negotiable for Agent Systems
Auto-instrumentation is great for standard web apps, but it can't see your domain logic. Every agent operation needs an explicit span with relevant attributes like agent.name, batch.index, and batch.size.
2. Batch Operations Need Tracking
If you're processing items in batches, track:
-
batch.index: Which batch is this? (1, 2, 3) -
batch.total: How many batches total? -
batch.size: How many items in this batch?
This let me discover that batch 2 was consistently 20% slower than batches 1 and 3, which pointed to a data distribution problem.
3. P95/P99 Reveals What Average Hides
Average latency was 3.33 minutes, but P99 was 13.33 minutes. If I'd only looked at averages, I would have thought performance was fine. The high percentiles exposed the LLM variance issue.
4. Resource Attributes Speed Up Queries
I tagged all data with service.name as a resource attribute (not a span attribute). SigNoz queries filter on resource attributes first, making every query significantly faster. This is in the docs but easy to miss.
5. MCP Integration Changed My Workflow
Being able to describe dashboards in natural language ("show me P50/P90/P95/P99 on one chart") instead of configuring query builders saved hours. The MCP server turned dashboard creation from a chore into a conversation.
The Results
Before instrumentation:
- No visibility into which agent was slow
- Had to add print statements and rerun 15-minute workflows to debug
- Unknown if errors were happening in production
After instrumentation:
- Identified Epic Refinement as the bottleneck (5.77 min average)
- Discovered batch 2 consistently slower than batches 1 and 3
- Caught transient 4% error spike that self-resolved
- P99 latency tracking revealed 5x variance in LLM performance
- Alerts catch failures within 1 minute
Most importantly, I now have actionable optimization targets:
- Epic Refinement could be parallelized to reduce the 5.77 min bottleneck
- Investigate why batch 2 is slower (probably contains more complex epics)
- Consider LLM response caching to reduce P99 variance
Conclusion
Instrumenting a multi-agent LLM system taught me that observability isn't just about adding traces. It's about choosing the right attributes to track (like batch index), measuring the right percentiles (P95/P99, not just average), and building dashboards that answer specific questions ("which agent is slow?" not "here's all the data").
The combination of OpenTelemetry's flexibility and SigNoz's MCP integration made this project possible in a weekend. If you're building with LLM agents, start with manual spans around each agent operation and work from there.
Tech Stack: Flask, LangChain, Ollama (Mistral), OpenTelemetry, SigNoz Foundry
Dashboards: 4 dashboards, 7 alerts, all created via MCP



Top comments (0)