Emit secrets_read billable event from the Rails OpenBao audit endpoint
What
Emit a secrets_read billable usage event from the Rails OpenBao HTTP
audit-backend ingest endpoint, using the merged billing module
(Gitlab::BillingEvents::Client, !240688 (merged)). Emission is SaaS-only and gated
behind a default-off gitlab_com_derisk feature flag (secrets_manager_emit_secret_read_events).
Why
This MR is the producer side of an already-aligned contract. The CDot consumer
(https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/15627, merged into 19.0) already registers
secrets_read as a billable event type, routes it through
Billing::Conversion::UsageCreditCalculator with the quantity_based_rate
category, and ships a placeholder credit ratio in
config/billing/pricing_multipliers.yml. CDot prices these events the moment
Rails starts emitting them.
Originally this MR introduced a bespoke Gitlab::Tracking::BillableUsageTracker
Gitlab::Tracking::BillableRealmon top ofGitlab::InternalEvents. Since then the dedicated billing module landed on master (!240688 (merged)), so this MR is rewritten to call it directly and the bespoke tracker/realm classes are dropped. The billing client owns realm/deployment_type,subject/global_user_id, and deterministicevent_idgeneration, and is independently gated byFeature.enabled?(:billing_event_tracking, :instance).
How
-
Emitter —
SecretsManagement::BillableEvents::SecretsReadEmitterclassifies the parsedSecretsManagement::AuditLogand emits viaGitlab::BillingEvents::Client.track_billing_event. Guard chain inemit!:saas?—::CloudConnector.gitlab_realm == ::CloudConnector::GITLAB_REALM_SAAS(SM/Dedicated revert to flat-rate pricing, so the monolith emits nothing there).feature_enabled?— thegitlab_com_deriskFF, scoped to the project/group/:instance.billable?— dropstype: "request"pre-operation log entries, restricts tosecrets_manager_read_project_secret/secrets_manager_read_group_secret, and requiresauth.policy_results.allowed == true(successful reads only).return unless billing_namespace— the client readsnamespace.idunconditionally;billing_namespaceisproject.namespace(project mount) or the group (group mount).
Call arguments:
event_type: "secrets_read",category: self.class.name,unit_of_measure: "request",quantity: 1,namespace,project,user: audit_log.author,idempotency_key: "secrets_read:<request.id>",timestamp(parsed from the audit logtime), and a compactedmetadatahash (pipeline_id,job_id,mount_type,audit_request_id,openbao_entity_id). Arescue StandardErrorreports to error tracking and swallows, so a billing-emit failure never propagates. -
Synchronous emission, inline at the API request — hooked from the Grape endpoint
POST /api/v4/internal/secrets_manager/audit_logsafteraudit_log.log!. No worker, no queue — the Snowplow call is an in-process buffer append. A defense-in-depthbegin/rescuearound the emit call ensures a billing-emit failure never turns into a non-2xx response (which would trigger an OpenBao HTTP-audit-backend retry and double-bill). -
Deduplication — the OpenBao
request.idUUID rides on the event as theidempotency_key. The billing client derivesevent_id = uuid_v5(instance_uuid, idempotency_key), so a replay of the same OpenBao request collapses to one billing event downstream. The client's own Rails → billing-endpoint retry handling is orthogonal to OpenBao → Rails retries; only the caller can see the upstream key, which is why we supply it.
The captured-from-GDK fixtures
(audit_log_kv_data_read_success.json, audit_log_kv_data_read_group_secret_success.json,
audit_log_kv_read_permission_denied.json) pin these field names against actual
OpenBao output and close a real fixture gap in the repo.
Out of scope
- Analytics / Service Ping — the billing module emits its own
usage_billing_eventinternal event in a follow-up Analytics MR; the Service Ping metric is re-added there on top of that event. No event YAML or metric definition ships here. - Non-SaaS emission — pending a trusted, GitLab-owned emission path for self-managed.
Coordination with sibling MR !238145 (merged) (secrets_stored)
The earlier plan had this MR introduce the shared BillableUsageTracker /
BillableRealm for !238145 (merged) to consume. That coupling is gone: both events now
target the merged billing module directly, so there is no longer a hard
merge-order dependency between the two MRs.
Test plan
- RuboCop clean on touched files.
- RSpec green: emitter spec (project / group / permission-denied / FF-off / SM-Dedicated / request-log / non-read / raise-swallowed) and the amended request spec (API → emitter wiring + end-to-end SaaS billing call).
- Real OpenBao payloads captured from a local GDK with the file audit device enabled; the JSON fixtures are derived from that capture (tokens redacted, PII placeholdered).
- End-to-end against a GDK with the HTTP audit device pointed at the Rails
ingest endpoint, both FFs on, verifying the
billable_usageevent lands in Snowplow Micro. - Database review N/A — no migrations, no schema changes.
Screenshots
N/A — no UI change.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
- I have evaluated the MR acceptance checklist for this MR.
References
- Closes https://gitlab.com/gitlab-org/gitlab/-/work_items/600967
- Builds on the merged billing module !240688 (merged)
- CDot consumer side: https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/15627
- Sibling producer MR
secrets_stored: !238145 (merged) - Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/597888 (parent discussion; Proposal B accepted)
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/599011 and https://gitlab.com/gitlab-org/gitlab/-/work_items/597867 (sibling
secrets_stored) - Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/594638 (final credit ratio)