Commit 80763f7
feat(observability): RPC + workflow + DB-pool metrics for the agent JSON-RPC endpoint (#379)
## Summary
Brings the agent JSON-RPC endpoint (`POST /agents/{id}/rpc` —
`task/create` / `message/send` / `event/send`) up to the metrics
observability contract, and adds high-signal Postgres connection-pool
saturation metrics. Everything here is OTel-only (no StatsD dual-emit —
these are new series with no existing consumer), and a cheap no-op when
no OTLP endpoint is configured.
### RPC RED metrics (`agentex.rpc.*`)
- `agentex.rpc.request.duration` (timed dispatch → final byte, so
streaming responses are covered end-to-end), `agentex.rpc.requests`,
`agentex.rpc.errors`.
- Attributes follow the OTel RPC semantic conventions: `rpc.system`,
`rpc.method`, `rpc.jsonrpc.error_code`, `error.type`, `streaming`. No
high-cardinality identifiers (no task/agent/request ids). JSON-RPC
failures return HTTP 200, so the error code rides on
`rpc.jsonrpc.error_code` (set only on failure), not an HTTP status.
- Never-raise emission; a single terminal emit per request covering the
whole stream.
### Workflow-level GenAI metric
- `gen_ai.workflow.duration` for `task/create`, labeled
`gen_ai.operation.name=invoke_workflow`, so workflow-level duration is
directly queryable instead of only inferable from RPC duration.
`invoke_agent` stays reserved for the agent loop's own duration, so the
two operation names never mix gateway and in-pod durations.
### DB connection-pool metrics (OTel DB semconv)
- `db.client.connection.wait_time`,
`db.client.connection.pending_requests`, `db.client.connection.timeouts`
— the pre-outage early-warning signals for pool exhaustion.
- Sourced from an `InstrumentedAsyncAdaptedQueuePool` that wraps
`Pool.connect()` (the single, non-recursive acquisition entry point, run
inside the acquiring greenlet). The existing checkout/checkin listeners
can't produce these: by the time `checkout` fires the wait is over,
there's no waiter count, and a pool timeout raises before any connection
is handed out.
- `wait_time` uses explicit second-scale histogram buckets so quantiles
stay usable (the SDK default boundaries assume a millisecond unit).
## Testing
- Unit tests in `tests/unit/utils/test_rpc_metrics.py` and
`test_db_metrics.py`: unconfigured no-op, never-raise on backend faults,
metric name/attribute assertions, and the pool success / timeout /
non-timeout-error paths. All green; `ruff` clean.
## Notes / follow-ups
- Requires an OTLP endpoint configured on the pods to export.
- HTTP auto-instrumentation (route-templated
`http_server_request_duration_seconds`) lands on a separate branch.
- Dashboard/Mimir verification and matrix backfill to follow once the
target environment is available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds OTel-only RED metrics for the agent JSON-RPC endpoint
(`agentex.rpc.*`), a GenAI workflow-duration histogram for
`task/create`, and Postgres connection-pool acquisition metrics
(`db.client.connection.wait_time/pending_requests/timeouts/failures`)
sourced from a new `InstrumentedAsyncAdaptedQueuePool` subclass. It also
removes the `namespace: agentex` from the local OTel collector's
Prometheus exporter to fix a double-prefix bug that would have appeared
for new `agentex.*`-prefixed instrument names.
- **RPC metrics**: A synchronous `@contextmanager`
(`rpc_request_timing`) wraps both sync and streaming handlers; a
`RpcCallOutcome` dataclass lets handlers classify JSON-RPC error codes,
with `BaseException` catching client disconnects (e.g., `GeneratorExit`)
separately from structured RPC errors. Never-raise contract is enforced
throughout.
- **DB pool metrics**: `InstrumentedAsyncAdaptedQueuePool.connect()` is
the correct interception seam (checkout/checkin events can\u2019t see
the wait or a timeout), instruments are attached post-construction and
carried across `recreate()`, and the `wait_time` histogram uses explicit
second-scale buckets to keep quantiles usable.
- **Collector config**: Removing `namespace: agentex` aligns local
PromQL with the production Mimir pipeline but renames existing
`agentex_db_*` local series to `db_*`; teams with local Grafana
dashboards querying the old prefixed names will need to update their
queries.
<details><summary><h3>Confidence Score: 5/5</h3></summary>
Safe to merge; all changes are additive instrumentation with a
never-raise emission contract and no mutations to the core RPC execution
path.
The RPC and DB pool metric additions are purely observational. The
rpc_request_timing contextmanager correctly handles BaseException
including GeneratorExit, the pool override degrades to a passthrough
when OTel is unconfigured, and recreate() carries instrumentation
forward. Unit tests cover the no-op path, never-raise contract,
double-count regression, and the streaming GeneratorExit scenario.
**Files Needing Attention:** No files require special attention. The
otel-collector-config.yaml namespace removal warrants a local dashboard
audit if Prometheus queries exist against the old agentex_db_*-prefixed
series.
</details>
<details><summary><h3>Important Files Changed</h3></summary>
| Filename | Overview |
|----------|----------|
| agentex/src/utils/rpc_metrics.py | New module: RPC RED metrics and
workflow-duration histogram with correct OTel semconv, never-raise
emission, and single-record-per-request guarantee via contextmanager. |
| agentex/src/utils/db_metrics.py | Adds
InstrumentedAsyncAdaptedQueuePool and three new OTel
connection-acquisition metrics; pending_requests increment occurs
outside the try block, so an OTel SDK raise there leaves the counter
unbalanced without a compensating decrement. |
| agentex/src/api/routes/agents.py | Wraps sync and streaming RPC
handlers with rpc_request_timing; streaming handler correctly carries
instrumentation across yields and GeneratorExit. |
| agentex/src/config/dependencies.py | Adds
poolclass=InstrumentedAsyncAdaptedQueuePool to all three engine
constructors; straightforward wiring change. |
| agentex/otel/otel-collector-config.yaml | Removes namespace: agentex
from Prometheus exporter to prevent double-prefixing for agentex.*
metrics; renames existing local db.* metric series as a side effect. |
| agentex/tests/unit/utils/test_rpc_metrics.py | Comprehensive unit
tests covering no-op, never-raise, attribute correctness, double-count
regression, GeneratorExit, and handler-classification precedence. |
| agentex/tests/unit/utils/test_db_metrics.py | Unit tests for the pool
instrumentation covering passthrough, success, timeout, non-timeout
error, recreate propagation, and end-to-end wiring. |
</details>
<details><summary><h3>Sequence Diagram</h3></summary>
```mermaid
sequenceDiagram
participant Client
participant agents_py as agents.py
participant rpc_timing as rpc_request_timing
participant use_case as agents_acp_use_case
participant otel as OTel SDK
Client->>agents_py: "POST /agents/{id}/rpc"
agents_py->>rpc_timing: __enter__ (start timer)
rpc_timing-->>agents_py: RpcCallOutcome
alt Sync request
agents_py->>use_case: handle_rpc_request()
use_case-->>agents_py: result_entity
agents_py->>rpc_timing: __exit__ (normal)
rpc_timing->>otel: record duration/requests
agents_py-->>Client: AgentRPCResponse (200)
else Streaming request
agents_py->>use_case: handle_rpc_request()
use_case-->>agents_py: AsyncIterator
loop Each chunk
agents_py-->>Client: NDJSON chunk (yield)
end
alt Stream error
agents_py->>rpc_timing: rpc_call.fail(-32603, e)
agents_py-->>Client: error frame (yield)
end
agents_py->>rpc_timing: __exit__ (GeneratorExit or normal)
rpc_timing->>otel: record duration/requests/errors
end
Note over agents_py,otel: task/create also emits gen_ai.workflow.duration
```
</details>
<sub>Reviews (6): Last reviewed commit: ["Merge branch 'main'
into
stephen-wang/rp..."](0e754eb)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=48001757)</sub>
<!-- /greptile_comment -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 32a9acc commit 80763f7
7 files changed
Lines changed: 945 additions & 94 deletions
File tree
- agentex
- otel
- src
- api/routes
- config
- utils
- tests/unit/utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
25 | 30 | | |
26 | 31 | | |
27 | | - | |
28 | 32 | | |
29 | 33 | | |
30 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
46 | 49 | | |
47 | 50 | | |
48 | 51 | | |
| |||
549 | 552 | | |
550 | 553 | | |
551 | 554 | | |
552 | | - | |
553 | | - | |
554 | | - | |
555 | | - | |
556 | | - | |
557 | | - | |
558 | | - | |
559 | | - | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
560 | 564 | | |
561 | | - | |
562 | | - | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
563 | 569 | | |
564 | | - | |
565 | | - | |
566 | | - | |
567 | | - | |
568 | | - | |
569 | | - | |
570 | | - | |
571 | | - | |
572 | | - | |
573 | | - | |
574 | | - | |
575 | | - | |
576 | | - | |
577 | | - | |
578 | | - | |
579 | | - | |
580 | | - | |
581 | | - | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | | - | |
586 | | - | |
587 | | - | |
588 | | - | |
589 | | - | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
590 | 596 | | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
599 | 611 | | |
600 | 612 | | |
601 | 613 | | |
| |||
608 | 620 | | |
609 | 621 | | |
610 | 622 | | |
611 | | - | |
612 | | - | |
613 | | - | |
614 | | - | |
615 | | - | |
616 | | - | |
617 | | - | |
618 | | - | |
619 | | - | |
620 | | - | |
621 | | - | |
622 | | - | |
623 | | - | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
624 | 634 | | |
625 | 635 | | |
626 | | - | |
627 | | - | |
628 | | - | |
629 | | - | |
630 | | - | |
631 | | - | |
632 | | - | |
633 | | - | |
634 | | - | |
635 | | - | |
636 | | - | |
637 | | - | |
638 | | - | |
639 | | - | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
640 | 640 | | |
641 | | - | |
642 | | - | |
643 | | - | |
644 | | - | |
645 | | - | |
646 | | - | |
647 | | - | |
648 | | - | |
649 | | - | |
650 | | - | |
651 | | - | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
657 | | - | |
658 | | - | |
659 | | - | |
660 | | - | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
661 | 677 | | |
662 | 678 | | |
663 | 679 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
23 | 26 | | |
24 | 27 | | |
25 | 28 | | |
| |||
97 | 100 | | |
98 | 101 | | |
99 | 102 | | |
| 103 | + | |
100 | 104 | | |
101 | 105 | | |
102 | 106 | | |
| |||
109 | 113 | | |
110 | 114 | | |
111 | 115 | | |
| 116 | + | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
188 | 193 | | |
189 | 194 | | |
190 | 195 | | |
| 196 | + | |
191 | 197 | | |
192 | 198 | | |
193 | 199 | | |
| |||
0 commit comments