ORK Metrics Reference
ORK nodes export performance metrics via OpenTelemetry SDK → OTLP gRPC → Azure Managed Prometheus → Azure Managed Grafana. This page documents the metric naming conventions, label vocabularies, and the full dictionary of metric series an ORK emits.
If you are debugging an ORK in production, reading these will let you write PromQL queries directly against Azure Managed Prometheus or interpret what the staging Grafana dashboards are showing.
Pipeline overview
.NET side: ORK (Cryptide, master-libs, Ork.Forseti, Tide.Ork.* meters)→ OTel SDK, OTLP gRPC :4317 ─┐├─► OTel Collector ─remote-write (Entra OAuth2)─► Azure Managed Prometheus ─PromQL─► Azure Managed GrafanaJVM side: TideCloak (override) + idp-extensions + Midgard's Java facade→ /metrics:9000 ──scrape──────┘
The collector runs as an Azure Container App sidecar. ORKs push via OTLP gRPC; TideCloak is scraped from :9000. Cardinality is enforced at the collector edge (see Cardinality discipline below).
OTel → Prometheus naming
All metrics use seconds as the unit. On Prometheus export the standard OTel mapping applies:
- dots become underscores:
tide.crypto.duration→tide_crypto_duration - histograms get the
_seconds(or_bytes, etc.) unit suffix and produce three Prometheus series:_count(cumulative observation count)_sum(cumulative sum of observed values)_bucket(with thelelabel, for percentile computation viahistogram_quantile)
- counters get an
_errorsor_totalsuffix where applicable
Example:
OTel name: tide.crypto.duration (Histogram<double>, unit: s)Prometheus: tide_crypto_duration_seconds_counttide_crypto_duration_seconds_sumtide_crypto_duration_seconds_bucket{le="..."}
When writing PromQL or referencing metrics outside of code, always use the Prometheus form (underscores, _seconds suffix). Within ORK source the dotted OTel form is used because that is what the SDK consumes.
Cardinality discipline
This is a hard rule: never put unbounded values into metric labels. Use the centralized tag helpers in Cryptide.Metrics.* and Tide.Ork.* meter accessors — they accept only enums and validated strings from closed vocabularies.
Forbidden as labels (would explode AMP cardinality, drives up cost, breaks dashboards):
- user id, vuid, vendorId, clientId
- request id, session id, traceparent
- raw URLs
- realm (unless provably bounded in your environment)
- error messages (use a code; the message goes in the log)
Allowed as labels (bounded enums or closed vocabularies):
typefrom the crypto vocabularyopfrom the protocol op vocabulary (keygen,sign,check_payment,voucher, ...)phase(e.g.GenShard,SendShard,Commit)result∈success/error/skippedoutcome∈success/error/gas_exhausted/crash/canceled/pool_timeoutscheme— when needed alongsidetype, but kept enumerableside—server/browser/vendorstatus— HTTP status class or normalized outcome string
node and role are OTel resource attributes, not per-metric labels — they apply to every metric the SDK exports from an ORK process and are added once at startup. Do not duplicate them per metric.
The collector edge runs a Micrometer/OTel filter that drops Keycloak's high-cardinality base_* and http_server_requests_* defaults before they reach AMP — those are explicit denials in the collector config, not allowlist gaps.
Metric dictionary
All metric names below are shown in the dotted OTel form. To use them in PromQL, swap dots for underscores and add the appropriate suffix (see OTel → Prometheus naming).
Cryptographic primitives
| Metric | Type | Labels | Emitter |
|---|---|---|---|
tide.crypto.duration | Histogram | type, scheme, side | Cryptide (in Key/TideSignature.cs dispatch + scheme delegates), Midgard pure-crypto, override (JWS / SignModel / SignWithVrk) |
tide.crypto.count | Counter | type, scheme, side | Cryptide |
tide.crypto.errors | Counter | type, scheme, side | Cryptide |
The type label values are drawn from the crypto / signing type vocabulary.
ORK protocol flows
| Metric | Type | Labels | What it measures |
|---|---|---|---|
tide.ork.protocol.duration | Histogram | op, phase | End-to-end protocol flow duration per op (keygen GenShard / SendShard / Commit, sign PreSign / Sign, decrypt, offboard, anonsign, voucher) |
tide.ork.request.duration | Histogram (auto) | route, status | ASP.NET HTTP request handling, auto-instrumented via AddAspNetCoreInstrumentation() |
tide.ork.outbound.duration | Histogram | target, endpoint, status | Outbound calls from this ORK to peers / master / payer. endpoint is a normalized logical name (never a raw URL). |
tide.ork.throttle.rejections | Counter | (none) | App-level shedding when the ORK refuses a request before doing work |
tide.ork.sync_semaphore.wait | Histogram | (none) | Time waiting for the SyncService semaphore |
Master node coordination (master-libs)
Only emitted by master-role ORKs (sork1/sork2 in the staging topology).
| Metric | Type | Labels | What it measures |
|---|---|---|---|
tide.master.op.duration | Histogram | op, phase, result | Per-operation duration. Phase splits: Register* → network / crypto / db; SubmitUser → validation / aggregation / lock / db |
tide.master.op.count | Counter | op, result | Total operation count |
tide.master.lock.wait | Histogram | op | Time waiting to acquire the master lock |
tide.master.lock.hold | Histogram | op | Time holding the master lock once acquired |
tide.master.lock.in_flight | UpDownCounter | op | Currently-held locks (per op kind) |
tide.master.registry.size | ObservableGauge | kind | Size of the in-memory registry (active rows by kind) |
registry.size is hosted by an ORK-side hosted service rather than by master-libs itself — master-libs would otherwise need to capture a scoped DbContext, which is not its responsibility.
Forseti policy execution engine
Forseti is the in-ORK sandboxed C# policy executor. See the Forseti concept page for background.
| Metric | Type | Labels | What it measures |
|---|---|---|---|
tide.ork.forseti.pool.queue_depth | ObservableGauge | (none, gauge per pod) | VmHost process pool: callers currently queued waiting for a free child process. Saturation signal. Unit: callers |
tide.ork.forseti.compile.duration | Histogram | outcome | Source-to-DLL compile time via the sandboxed VmHost compile path. outcome ∈ success / error |
tide.ork.forseti.execute.duration | Histogram | outcome, stages | Per-policy-execution latency in the sandbox. outcome ∈ success / error / gas_exhausted / crash / canceled / pool_timeout. stages ∈ ValidateData / ValidateApprovers / ValidateExecutor |
Notes on Forseti histogram behaviour:
compile.durationexports only after the first observation. Fresh sork pods do not emit acompile_duration_*series until a contract is uploaded or the pod runs its startup warmup compile. A series being absent in AMP is not necessarily a bug; it may mean no compile has happened yet on that pod.execute.durationfollows the same rule. Until aPolicyAuthorizationFlow-driven request lands on the pod,execute_duration_*will not exist in AMP.queue_depthis a gauge, so it exports0continuously even when idle — it is the only Forseti metric that is always visible.
TideCloak / IDP / Midgard (cross-tier, JVM)
These are emitted by the TideCloak + idp-extensions + Midgard Java code and reach AMP via the :9000 scrape path rather than OTLP gRPC.
| Metric | Type | Labels | Emitter |
|---|---|---|---|
tide.midgard.op.duration | Histogram | op, composite, result | Midgard. Composite ops are end-to-end including ORK round-trip |
tide.token.issue.duration | Histogram | path, alg, result | tidecloak-override token issuance |
tide.idp.login.duration | Timer | phase, sso, result | tidecloak-idp-extensions, stops at callback.authenticated() hand-off — token issuance itself is core Keycloak |
tide.idp.login.callback.hop.duration | Timer | hop ∈ decrypt / verify, result | idp-extensions, around the two callback Midgard calls |
tide.idp.voucher.duration | Timer | action, source, result | idp-extensions — action validated against a closed allow-list before recording |
tide.idp.vrk_task.duration | Timer | task, result | idp-extensions scheduled VRK tasks |
.NET runtime saturation (auto-instrumented)
These come free from the .NET runtime instrumentation registered via AddRuntimeInstrumentation() in Program.cs. ASP.NET, HttpClient, GC, and ThreadPool are all instrumented automatically.
Notable series under the process_runtime_dotnet_* prefix:
process_runtime_dotnet_gc_heap_size_bytes(bygeneration∈ gen0 / gen1 / gen2 / loh / poh)process_runtime_dotnet_gc_collections_count_total(by generation)process_runtime_dotnet_thread_pool_queue_lengthprocess_runtime_dotnet_thread_pool_threads_countprocess_runtime_dotnet_exceptions_count_totalprocess_runtime_dotnet_monitor_lock_contention_count_total
ASP.NET HTTP-side auto-instrumentation gives http_server_active_requests etc., scoped to the job=sork-env/ork label set by resource attributes.
Crypto / signing type vocabulary
Shared between browser and server crypto, so the same type label can be compared across SWE-side and ORK-side metrics in the same dashboard query.
Signing:
eddsa_standard, eddsa_partial_threshold, eddsa_group, eddsa_blind_partial, vrk_ed25519, threshold_vvk (sign_model), anon_sign, jws_eddsa
Verify:
eddsa_verify, blind_verify, doken_verify, commit_verify
Encryption:
elgamal, elgamal_decentralized, aes_gcm, ecdh, asymmetric
Key derivation:
nested_sss_keygen, sss_share, lagrange_eval, vrk_keygen, sha256_derive, hash_to_point
When adding a new crypto operation to Cryptide that needs instrumentation, extend this vocabulary in Cryptide/Metrics/CryptideMetrics.cs (do not introduce a new free-form string at the call site). The Prometheus dashboards build PromQL queries that filter and group by type, so the values need to be stable.
Grafana dashboards
The metrics above are surfaced in two staging/prod dashboards, tracked under version control in tidecloak-override/Tidified/grafana/dashboards/:
tide-metrics-dashboard.json— Tide Network Performance (crypto primitives, ORK protocol, ORK outbound, master, ORK HTTP, .NET runtime, TideCloak workflows, Forseti)tide-ork-logs-dashboard.json— ORK logs (Serilog → Azure Monitor Logs)
Distinct from the local-only HA stack provisioning under Tidified/ha-tidecloak/grafana/. Edit the tracked JSON via PR; manually re-import into Azure Managed Grafana after merge (Grafana detects the existing UID and offers overwrite).
When adding a new dashboard panel for a newly-instrumented metric, edit the tracked JSON — do NOT edit the version in Grafana's UI and forget to push back. Conventions for new panels are documented in the Tidified/grafana/README.md alongside the dashboard JSON.
Adding a new metric (developer checklist)
- Pick the right meter. Cryptide primitives go in
Cryptide.Metrics.CryptideMetrics. ORK-side protocol/outbound/Forseti go inTide.Ork.*. Master coordination goes inTide.Master. - Choose the right instrument type — Histogram for latency, Counter for one-way totals, ObservableGauge for poll-style readings, UpDownCounter for in-flight gauges.
- Pick labels from a bounded vocabulary (see Cardinality discipline). If you find yourself wanting a per-user or per-request label, the answer is no — log it instead.
- Wire the meter into
Program.cs—AddMeter("Tide.Ork.YourSubsystem")so OTel SDK actually exports it. - Verify locally that the dotted OTel name maps to the expected Prometheus name (dots → underscores,
_secondssuffix for time histograms). - Add a Grafana panel to the tracked dashboard JSON if the metric should be on the standard dashboard. Use existing panels' fieldConfig + options blocks as a template.
- Document the metric in this dictionary (the file you are reading).
Related references
- Naming + label vocabulary architecture: this page
- Error reporting reference: ORK Errors
- Forseti concept: Programmable Policy
- Tide whitepaper — Authority in Action: tide.org/whitepaper/authority-in-action