> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Observability

> Metrics, traces, logs and health endpoints Veriqa exposes, and how to export them with OpenTelemetry.

Veriqa is instrumented with the standard .NET primitives — `System.Diagnostics.Metrics` and
`System.Diagnostics.ActivitySource` — and exports nothing by itself. The components own their
instruments; the host subscribes to them and decides where the data goes. The names on this page
are a public contract: build dashboards and alerts on them.

| Signal | Name to subscribe to |
| - | - |
| Metrics (meters) | `Veriqa.Core.TransactionEngine`, `Veriqa.Core.ChannelAdapter` |
| Traces (activity sources) | `Veriqa.Core.TransactionEngine`, `Veriqa.Core.ChannelAdapter` |

## Turning the export on

<Tabs>
  <Tab title="Self-hosted image">
    The image already subscribes to both meters and both activity sources, together with the
    ASP.NET Core, `HttpClient` and .NET runtime instrumentation. The export starts when the
    standard OpenTelemetry variable names a collector:

    ```bash theme={null}
    OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
    ```

    Without the variable nothing is exported. With it, metrics, traces and logs all go over OTLP;
    the other standard `OTEL_EXPORTER_OTLP_*` variables (protocol, headers) and `OTEL_SERVICE_NAME`
    are read by the OpenTelemetry SDK as usual.

    The image has no scrape endpoint of its own — there is no `/metrics`. For Prometheus, point the
    image at an OpenTelemetry Collector and let the collector expose or push the metrics.
  </Tab>

  <Tab title="Embedded in your host">
    `AddVeriqaAuthServer` registers the instruments and nothing else: your host owns the
    OpenTelemetry pipeline. Add the two names to it — the constants live in the MIT-licensed
    `Veriqa.Core.Contracts` package:

    ```csharp theme={null}
    using OpenTelemetry.Metrics;
    using OpenTelemetry.Trace;
    using Veriqa.Core.ChannelAdapter.Constants;
    using Veriqa.Core.TransactionEngine.Constants;

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics => metrics
            .AddMeter(TransactionTelemetry.MeterName)
            .AddMeter(ChannelTelemetry.MeterName))
        .WithTracing(tracing => tracing
            .AddSource(TransactionTelemetry.ActivitySourceName)
            .AddSource(ChannelTelemetry.ActivitySourceName));
    ```

    Exporters, sampling and the ASP.NET Core instrumentation are yours to choose. A host with no
    OpenTelemetry pipeline pays nothing for the instruments and gets no data.
  </Tab>
</Tabs>

## Metrics

### Transactions — meter `Veriqa.Core.TransactionEngine`

| Metric | Type | Unit | Tags |
| - | - | - | - |
| `veriqa.transaction.initiated` | counter | — | `veriqa.transaction.type` |
| `veriqa.transaction.terminated` | counter | — | `veriqa.transaction.outcome`, `channel_type` |
| `veriqa.transaction.duration` | histogram | `s` | `veriqa.transaction.outcome` |

* **`initiated`** counts transactions that entered the lifecycle; **`terminated`** counts the ones
  that reached a terminal state, once each — a repeated terminal transition is not counted twice.
* **`duration`** is the lifetime from creation to the terminal state. With a user in the loop it is
  dominated by how long the person takes to answer, not by the server.
* The sign-in funnel is `initiated` against `terminated` with the outcome `completed`; the refusal
  share is the share of the other outcomes, `superseded` aside — that one is a repeated attempt, not
  a refusal, and is kept apart from `failed` so that retries do not raise the share of failures.

| Tag | Values |
| - | - |
| `veriqa.transaction.type` | `login` — a sign-in; `confirmation` — a [server-to-server confirmation](/docs/reference/confirmation-api) |
| `veriqa.transaction.outcome` | `completed` — confirmed and finished; `declined` — the user refused; `expired` — no answer within the TTL; `failed` — a failure that is not the user's refusal; `superseded` — a sign-in closed because the same user [started a newer one](/docs/concepts/auth-flow#starting-the-sign-in-again): in events and the audit trail it is a failed transaction with the reason `superseded_by_new_sign_in` |
| `channel_type` | The type of the channel that answered, else of the channel that was asked; `unknown` when the transaction ended with no channel involved — typically an expiry before anyone answered |

<Note>
  `completed` is deliberately not `confirmed`. The outcome shown to the user in the messenger is
  "confirmed"; the engine records `completed` one step later, when the identity is resolved and
  stored. A sign-in confirmed in the channel can therefore still end as `failed`.
</Note>

### Channels — meter `Veriqa.Core.ChannelAdapter`

| Metric | Type | Tags | What it counts |
| - | - | - | - |
| `veriqa.channel.notice.failures` | counter | `channel_type`, `error_code`, `notice_kind` | Messages the channel failed to deliver to the user |
| `veriqa.channel.start.unserved` | counter | `channel_type` | Sign-in start events the core could not serve: the transaction they name is gone, its window has closed, or it no longer accepts the event |
| `veriqa.channel.event.foreign_transaction` | counter | `channel_type` | Channel events naming a transaction that belongs to a tenant other than the owner of the channel registration they arrived through |

* **`notice.failures`** is the one to alert on first: a growing series means users are not getting
  the confirmation prompt (`notice_kind` = `message`) or the final outcome (`notice_kind` =
  `outcome`). `error_code` is the adapter's error code, always from a code registry.
* **`start.unserved`** makes a silence visible. The user opened a stale link or scanned an old QR,
  and a channel with no reply text configured answers nothing — the counter still moves.
* **`event.foreign_transaction`** is answered to the sender exactly like an unknown transaction, so
  the answer reveals nothing. A sustained rate is someone trying identifiers that are not theirs.

<Note>
  **No identifiers in metric tags.** Tags never carry a transaction, session or channel-user
  identifier, a recipient or message text. Every tag is a closed set, so the number of time series
  is bounded by your channels and the values above.
</Note>

## Traces

| Span | Source | Opened when |
| - | - | - |
| `veriqa.transaction.create` | `Veriqa.Core.TransactionEngine` | A transaction is created — the root of the sign-in trace |
| `veriqa.channel.prompt.send` | `Veriqa.Core.ChannelAdapter` | The confirmation prompt leaves for the messenger |
| `veriqa.channel.inbound` | `Veriqa.Core.ChannelAdapter` | A channel event arrives over the webhook route |
| `veriqa.transaction.complete` | `Veriqa.Core.TransactionEngine` | The transaction is finalized from channel data: confirm, resolve the identity, complete |

| Span attribute | Meaning |
| - | - |
| `veriqa.transaction.id` | Identifier of the transaction the span acts on |
| `veriqa.transaction.type` | Transaction type, on `veriqa.transaction.create` |
| `veriqa.transaction.trace_id` | Trace identifier of the trace that created the transaction, on `veriqa.channel.inbound` |
| `channel_type` | Channel type, on the channel spans |
| `error_code` | Error code of the failure, on a span with the status `Error` |

**A sign-in is two traces, not one.** The user answers an arbitrary time after the prompt, and the
messenger propagates no trace context, so the inbound event opens a trace of its own. The two are
stitched by an attribute: `veriqa.transaction.trace_id` on the inbound span holds the trace
identifier of the trace that created the transaction. To get from the answer to the sign-in
request, search your backend by that value.

* For a sign-in the stored value is the trace identifier of the `/connect/authorize` request. If
  your application propagates `traceparent`, that is your application's own trace.
* For a server-to-server confirmation it is the `correlation_id` you passed in the request.
* The same value is written to the `CorrelationId` field of the audit record, so the audit trail
  and the traces are joined by it too.
* A host without tracing stores no value for a sign-in — no identifier is invented in its place —
  and the inbound span is then left without the attribute.

<Note>
  **Reading span status.** A span is marked `Error` for a failure that ends inside the operation it
  covers, with the reason in `error_code`. An unset status means "no failure ended here", not "the
  operation succeeded" — the outcome of the transaction is in the metrics and the audit trail.
</Note>

In `Polling` mode there is no inbound HTTP request and no `veriqa.channel.inbound` span; the
`veriqa.transaction.complete` span is still opened.

## Logs

In the self-hosted image logs follow the same switch as the other signals: with
`OTEL_EXPORTER_OTLP_ENDPOINT` set they are exported over OTLP, with the formatted message and the
logging scopes included. Log records written inside a span carry its trace and span identifiers, so
a log line leads to its trace.

What is and is not written to logs — and at which level — is covered in
[Security and data handling](/docs/guides/data-handling#logs).

## Health endpoints

These are the endpoints of the self-hosted image.

| Endpoint | Answers |
| - | - |
| `/health/live` | Liveness — the process is up |
| `/health/ready` | Readiness — configured dependencies answer. The probe for your orchestrator |
| `/health/channels` | Per-channel state. Always `200` — read the body, not the status code |
| `/health` | Every registered check at once |
| `/alive` | The process-level check alone |

Channels are deliberately absent from readiness: an unreachable messenger must not take a working
replica out of rotation. What `/health/channels` does and does not tell you is described in the
[self-hosted quickstart](/docs/quickstart/self-hosted#8-verify).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.