Bolty emits :telemetry events for the query and
connection lifecycle, so you can wire query latency, pool health, and error rates
into your own metrics/tracing without wrapping every call site.
Attach a handler at application start:
:telemetry.attach_many(
"bolty-logger",
[
[:bolty, :query, :stop],
[:bolty, :query, :exception],
[:bolty, :stream, :stop],
[:bolty, :connect],
[:bolty, :disconnect]
],
&MyApp.Telemetry.handle_event/4,
nil
)Durations are in :native time units — convert with
System.convert_time_unit(duration, :native, :millisecond).
Query events
Bolty.query/4, Bolty.query!/4, Bolty.query_many/4, and Bolty.query_many!/4
wrap the server round-trip in a :telemetry.span/3,
emitting the standard start/stop/exception trio under [:bolty, :query].
| Event | Measurements | Metadata |
|---|---|---|
[:bolty, :query, :start] | :monotonic_time, :system_time | :db_system, :db_statement, :db_instance, :telemetry_span_context |
[:bolty, :query, :stop] | :monotonic_time, :duration | :db_system, :db_statement, :db_instance, :result, (:error), :telemetry_span_context |
[:bolty, :query, :exception] | :monotonic_time, :duration | :db_system, :db_statement, :db_instance, :kind, :reason, :stacktrace, :telemetry_span_context |
Metadata keys:
:db_system— always"neo4j".:db_statement— the Cypher statement string.:db_instance— the target database from the:dboption, ornilfor the server default.:result—:okor:error, a coarse outcome status you can map straight onto a span status. A server-side failure (e.g. a Cypher error) is an:error:stop, not an:exception; the:exceptionevent fires only when a call raises (e.g. aDBConnection.ConnectionErroron a checkout/queue timeout).:error— present only when:resultis:error: the%Bolty.Error{}(or a%DBConnection.ConnectionError{}) returned to the caller.
Query parameters are not included in metadata — they routinely carry secrets.
Streaming events
Bolty.stream/3 is lazy and consumer-paced, so it does not use the eager
[:bolty, :query] span (which would close before any records are pulled).
Instead it emits three discrete events over the cursor's lifetime:
| Event | Measurements | Metadata |
|---|---|---|
[:bolty, :stream, :start] | :system_time | :db_system, :db_statement, :db_instance, :fetch_size |
[:bolty, :stream, :fetch] | :records | :db_system, :has_more |
[:bolty, :stream, :stop] | :duration | :db_system |
:startfires once when the stream'sRUNis declared;:fetchfires once per batch (PULL) with:records(the batch size) and:has_more(whether more batches follow);:stopfires once when the cursor is released, with the total wall-clock:durationof the stream.- Sum
:fetch:recordsfor the total streamed; pair:start/:stopfor timing. As with query events, the Cypher statement is included but parameters are not.
Connection events
| Event | Measurements | Metadata |
|---|---|---|
[:bolty, :connect] | :duration | :db_system, :bolt_version, :server_version, :connection_id |
[:bolty, :disconnect] | :system_time | :db_system, :connection_id |
Mapping to OpenTelemetry
The :db_* metadata keys mirror the OpenTelemetry
database semantic conventions
(dots become underscores, since :telemetry metadata keys are atoms), so a bridge
such as opentelemetry_telemetry
can translate an event into a span with minimal glue:
| Bolty metadata | OpenTelemetry attribute |
|---|---|
:db_system | db.system ("neo4j") |
:db_statement | db.statement |
:db_instance | db.instance |
These follow the classic db.* names; recent semconv revisions rename some
(db.statement → db.query.text, db.instance → db.namespace) — map them in
your handler if you target the newer spec.
[:bolty, :connect] fires once a connection is fully established (HELLO/LOGON
complete); :duration covers config parsing, the TCP/TLS connect, and the Bolt
handshake. [:bolty, :disconnect] fires when DBConnection tears a connection down.