Skip to content

Prometheus metrics

Availability All editions — Community and up.

The controller exposes Prometheus metrics at /metrics for monitoring sessions, proxy health, auth and integrations.

/metrics is off until you set TESSERA_METRICS_TOKEN — without it the endpoint returns 404. Generate a random value and pass it as a bearer token (see Configuration):

Terminal window
curl -H "Authorization: Bearer $TESSERA_METRICS_TOKEN" https://tessera.example.com/metrics

Scrape config:

scrape_configs:
- job_name: tessera
scheme: https
authorization:
credentials: <TESSERA_METRICS_TOKEN>
static_configs:
- targets: ["tessera.example.com"]
MetricTypeLabelsMeaning
tessera_build_infogaugeversionBuild version (value is always 1).
tessera_http_requests_totalcountermethod, codeAPI requests by method and status class.
tessera_http_request_duration_secondshistogram—API request latency.
tessera_http_requests_in_flightgauge—Requests currently being served.
tessera_sessions_started_totalcounterkindSessions created, by target kind.
tessera_sessions_activegaugekindCurrently active proxied sessions.
tessera_sessions_closed_totalcounterkindSessions ended, by target kind.
tessera_session_duration_secondshistogram—Session length.
tessera_k8s_requests_totalcounterverbProxied apiserver requests by verb.
tessera_commands_blocked_totalcounter—Commands/queries blocked in read-only mode.
tessera_proxy_errors_totalcounterkindProxy session errors by target kind.
tessera_access_requests_totalcountereventJust-in-time access requests by event.
tessera_auth_logins_totalcountermethod, outcomeLogin attempts by method and outcome (outcome is one of success, failure, blocked, disabled).
tessera_notifications_totalcounterchannel, outcomeNotification deliveries.
tessera_security_alerts_totalcounteractionSecurity alerts raised, by the audit event that raised them.
tessera_audit_export_totalcounteroutcomeAudit-export deliveries to your SIEM.

These seven describe the tamper-evident log itself: whether it is being sealed, whether it still adds up, and whether anything is being lost on the way in. See Audit log for what the chain does and does not cover.

MetricTypeMeaning
tessera_audit_last_seal_timestamp_secondsgaugeWhen a block was last sealed successfully. Set on success only, so its age is the signal.
tessera_audit_blocks_sealed_totalcounterBlocks sealed since start.
tessera_audit_seal_errors_totalcounterSealing attempts that failed.
tessera_audit_unsealed_rowsgaugeRows written but not yet in a block. A handful at any moment is normal.
tessera_audit_chain_problemsgaugeProblems found by the last full verification. Anything above zero is an incident.
tessera_audit_last_verify_timestamp_secondsgaugeWhen the chain was last verified in full. Survives a restart.
tessera_audit_write_errors_totalcounterAudit entries that could not be written at all.
tessera_audit_chain_unsignedgauge1 when no signing key is configured, so the chain is hash-linked only.

And one that is not about the audit log but belongs on the same dashboard:

MetricTypeMeaning
tessera_license_expires_timestamp_secondsgaugeWhen the current licence expires.

Plus the standard Go runtime and process collectors (go_*, process_*).

# Active sessions by kind
tessera_sessions_active
# API error rate
sum(rate(tessera_http_requests_total{code=~"5.."}[5m]))
/ sum(rate(tessera_http_requests_total[5m]))
# Blocked commands (read-only enforcement) over time
rate(tessera_commands_blocked_total[5m])
# Failed logins (anything that isn't a success — failure, blocked or disabled)
rate(tessera_auth_logins_total{outcome!="success"}[5m])

Seven rules. They are written out in full because an audit log nobody is alerted about is evidence that something happened, not a control: ISO/IEC 27001:2022 A.8.16 asks for monitoring, and monitoring means somebody is told.

These watch the machinery. The events themselves, somebody being handed administrative authority or a run of failed sign-ins, are announced by the controller into a chat instead, because they need a person rather than a dashboard: see Notifications. tessera_security_alerts_total is the same set counted, which is worth graphing rather than paging on.

groups:
- name: tessera-audit
rules:
# The log stopped being written. Nothing else on this page matters if this fires.
- alert: TesseraAuditWriteErrors
expr: increase(tessera_audit_write_errors_total[15m]) > 0
labels: { severity: critical }
annotations:
summary: "Tessera could not write {{ $value }} audit entries"
# The chain does not add up. Either the log was altered or the thing that proves it
# wasn't is broken, and both need a person.
- alert: TesseraAuditChainBroken
expr: tessera_audit_chain_problems > 0
for: 5m
labels: { severity: critical }
annotations:
summary: "Tessera audit chain reports {{ $value }} problem(s)"
# Nobody has checked. A chain_problems of zero that is two days old means "not looked at",
# which reads exactly like "fine" on a dashboard.
- alert: TesseraAuditNotVerified
expr: time() - tessera_audit_last_verify_timestamp_seconds > 172800
labels: { severity: warning }
annotations:
summary: "Tessera audit chain has not been verified for over 48h"
# The sealer has stopped. Rows keep being written and stop being protected, which is the
# state an attacker with database access can arrange on purpose.
- alert: TesseraAuditSealerStalled
expr: time() - tessera_audit_last_seal_timestamp_seconds > 600
for: 5m
labels: { severity: critical }
annotations:
summary: "No audit block sealed for over 10 minutes"
# The chain carries no signatures, so anyone who can write to the database can rewrite
# the log and have the check pass. Not an incident, a configuration gap: it fires once,
# stays until a key is set, and is deliberately kept out of chain_problems so that gauge
# keeps meaning "the log does not add up".
- alert: TesseraAuditChainUnsigned
expr: tessera_audit_chain_unsigned == 1
for: 1h
labels: { severity: warning }
annotations:
summary: "Tessera audit chain is unsigned — set TESSERA_AUDIT_SIGN_KEY"
# The SIEM is not receiving everything. Delivery is best effort by design, but the
# chain.sealed announcements are the only evidence that the newest blocks were not
# deleted, and a full buffer drops them like anything else.
- alert: TesseraAuditExportDropping
expr: increase(tessera_audit_export_total{outcome="dropped"}[15m]) > 0
labels: { severity: critical }
annotations:
summary: "Tessera dropped {{ $value }} audit events on the way to the SIEM"
# The licence, three weeks ahead, which is enough time to renew without a rush.
- alert: TesseraLicenceExpiring
expr: (tessera_license_expires_timestamp_seconds - time()) / 86400 < 21
labels: { severity: warning }
annotations:
summary: "Tessera licence expires in {{ $value | printf \"%.0f\" }} days"

Every exported event carries a seq and a boot id. seq counts up within one controller process, boot changes when it restarts, and the number is taken before the send, so an event that was dropped still consumes one. A gap in seq within a single boot is the receiver’s own proof that something did not arrive, independent of the alert above.

tessera_audit_chain_problems reports every problem found, not the number listed: the response caps the list it returns so that a systemic break does not produce an unreadable one, and the count is not capped with it.

tessera_audit_unsealed_rows is deliberately not an alert on its own. It rises and falls by design as blocks close, and TesseraAuditSealerStalled already catches the state where it stops falling. Graph it, do not page on it.

The controller walks the whole chain once a day, at 03:17 in its local time, and records the result in three places: the two metrics above, a chain.verified or chain.problem row in the audit log itself, and the line beside the Verify integrity button in the console.

Change the hour with TESSERA_AUDIT_VERIFY_AT (HH:MM), or set it to off and run the pass from your own scheduler against GET /api/audit/verify, which returns the same result as the button and updates the same metrics. Do one or the other, not both: a full walk reads every block and every row it covers, which is the heaviest read the controller makes. On an installation holding a hundred million rows it takes around eleven minutes, which is why it runs at night rather than on a timer during the day.