Send on-demand sync audits to S3, SQS, or SNS¶
Overview¶
When a PolicySync connector is configured with on-demand resource sync, it can publish one ON_DEMAND audit event per requested resource to the policysync_audit stream. Each event carries that resource's load outcome (found, added, updated, deleted, or not-found), the policies applied during the sync, and the request context.
These events flow through the same Audit Server path as every other connector audit. Once you turn the feature on for the connector, the ON_DEMAND events reach whichever destinations you have already configured for the policysync_audit stream — Apache Solr, Amazon S3 (through Audit Fluentd), Amazon SQS, and Amazon SNS (through an SNS→SQS subscription).
Because the stream is indexed into Apache Solr, the events are also visible in Privacera Portal alongside your other audit records.
The feature is opt-in and controlled by a single connector setting. When it is off, on-demand syncs still run and apply grants exactly as before — only the per-resource ON_DEMAND audit is not produced.
There are two parts to this configuration:
- Point the
policysync_auditstream at S3, SQS, or SNS using the standard destination configuration (Choose your destinations). - Enable the
ON_DEMANDaudit on the connector so the events are produced (Setup).
Prerequisites¶
On-demand resource sync is configured and working on the connector, so the connector is polling the Ops Server for RESOURCE_SYNC tasks.
Choose your destinations¶
Choose where the audits go before you turn the feature on, so the events have somewhere to land as soon as the connector starts producing them.
ON_DEMAND events travel on the policysync_audit stream, so they follow the destinations you set for that stream. There is no separate on-demand destination list — configure the outputs once, and the on-demand events are included automatically.
| Destination | How to configure |
|---|---|
| Amazon S3 | Route the stream through Audit Fluentd to S3. See Send Audits to S3, then optionally Query PolicySync Audits in Amazon Athena. |
| Amazon SQS | Enable the sqs destination and set the queue URL and region. See Send Audits to Amazon SQS. |
| Amazon SNS | Deliver to SQS from an SNS topic by subscribing your SQS queue to the topic, then configure the sqs destination as above. |
You also need AUDITSERVER_POLICYSYNC_AUDIT_HANDLER_V2_ENABLED: true. See Connector Audit Output Destinations.
Note
Make sure the policysync_audit destination list (AUDITSERVER_POLICYSYNC_AUDIT_DESTINATIONS_NAMES) includes the destinations you want. Privacera Manager does not add sqs for you — see Connector Audit Output Destinations.
Setup¶
Follow these steps on the host where Privacera Manager is installed.
- Sign in to that host.
- Open the Manager configuration directory:
Bash - Copy the connector sample variables into
custom-varsif you have not already. The-nflag avoids overwriting an existing file. Replace the file name with the one for your connector (for example,vars.connector.databricks.unity.catalog.yml).Bash - Edit the connector custom variables file:
Bash -
Enable the on-demand audit by adding (or updating) the following property. This renders the connector property
policysync.on-demand.audit.enable=true.Property Purpose Privacera Manager default CONNECTOR_ON_DEMAND_AUDIT_ENABLEEmit one ON_DEMANDaudit event per requested resource, on thepolicysync_auditstreamfalseLeaving this
falseonly suppresses the audit. Resource load, grant application, and the Ops Server task-status flow behave the same either way.
| YAML | |
|---|---|
- Confirm on-demand processing is enabled for the connector (
CONNECTOR_ON_DEMAND_PROCESSING_ENABLE: "true"); the audit is only produced when on-demand syncs actually run. - Apply the configuration with Privacera Manager, then restart the connector.
Event format¶
ON_DEMAND is one more eventType on the policysync_audit stream, so it uses the same wire contract as every other PolicySync connector event: one JSON object per event, UTF-8, camelCase property names, and UPPERCASE enum-like string values. Unknown root or extraInfo keys may appear in future releases — ignore or store them unless you explicitly handle them. See Policy Tracing and Explainability for the shared contract and the other event types.
The same payload shape is indexed into Apache Solr and forwarded to Amazon S3, Amazon SQS, and Amazon SNS.
Envelope fields¶
An ON_DEMAND event fixes several envelope fields to constant values, because it is always produced by the resource loader running from an on-demand task.
| Field | Type | Description |
|---|---|---|
eventType | String | Always ON_DEMAND. Filter on this field to select on-demand syncs |
evtTime | String | When the event was produced. ISO 8601, for example 2026-07-28T07:14:35Z |
timeTakenMs | Integer | How long the sync took, in milliseconds. Every event from the same sync carries the same value |
status | String | COMPLETED or FAILED for this one resource |
msg | String | Readable outcome for this resource, taken from the outcome code |
connectorType | String | Connector family, for example databricks_unity_catalog |
connector | String | Connector instance name |
changeLogId | Integer | Identifies the sync run. Every event from the same sync carries the same value |
changeLogType | String | Always SERVICE_RESOURCES |
loaderType | String | Always LOAD_SERVICE_RESOURCES |
triggerFrom | String | Always EVENT, meaning an Ops Server request triggered the sync rather than the schedule |
ondemandTaskId | Integer | Identifies the on-demand request. Use it to group every resource event from one request |
uuid | String | Document identifier assigned by the destination. May be null |
extraInfo | Object | The on-demand payload. Holds what happened and what was requested |
A few of these fields need more than a one-line description:
eventType— a scheduled resource load reportsLOADERinstead, so filtering onON_DEMANDselects only the syncs that were explicitly requested.status— the outcome for this one resource, not for the whole request. One request can produce a mix ofCOMPLETEDandFAILEDevents. See Outcome codes.msg— comes verbatim from the outcome code rather than being assembled at run time, so it is safe to show to a user. It never contains the target service's raw error; that is inerrorDetails.serviceError. The wording can change between releases, so code that branches on the outcome should readerrorCodeinstead.timeTakenMs— measured from the start of the resource load to the moment the event is produced.
Output: what the sync did¶
The extraInfo.output object describes what happened to this resource.
| Field | Type | Description |
|---|---|---|
resources | Object | The change counts for this resource, keyed by its type — table, schema, catalog, and so on |
policies | Array | The policies applied during this sync, each with its ACL counts |
policiesTruncated | String | Optional. Reads kept/total, for example 40/112, when the policy list was shortened to fit the audit size limit |
aclsExecuted | Object | Optional. ACL totals for this resource, added up across all its policies |
errorCode | String | Always present. The outcome code for this resource |
errorDetails | Object | Optional. Why the outcome failed |
When a field is empty rather than absent:
resourcesis{}when the resource was not found, when the load failed, or when the request could not be matched to a single resource.policiesis[]when this resource did not change, because no grants were applied to it.aclsExecutedis absent when the resource did not change. It is present on successful events too, not only on failures.errorDetailsis absent on a success, because there is nothing to report.
Resource counts¶
The value under output.resources.<type>. Each count is 0 or 1, because one event describes exactly one requested resource.
| Field | Type | Description |
|---|---|---|
found | Integer | 1 when the resource existed in the source. A deleted resource counts as found too, so found and deleted stay a consistent pair |
added | Integer | 1 when the resource was newly added to the connector store |
updated | Integer | 1 when an existing resource was updated |
deleted | Integer | 1 when the resource was removed from the source and its grants were revoked |
The policies that were applied¶
Each entry of output.policies:
| Field | Type | Description |
|---|---|---|
id | String | Policy identifier |
version | String | Policy version |
name | String | Policy name |
label | String | Correlation or trace label; empty when the policy carries none |
aclsExecuted | Object | ACL counts for this policy: total, applied, failed, skipped, and timeTakenMS |
failedReasons | Object | The target service's error text, mapped to how many ACL operations failed with it. {} when nothing failed |
skippedReasons | Object | Each skip reason, mapped to how many ACL operations were skipped for it. {} when nothing was skipped |
ownerIgnore | Object | Optional. How many ACL operations were skipped because the principal owns the resource |
ACL totals for this resource¶
The output.aclsExecuted object adds up the ACL operations for this resource across all of its policies. Read the counts from here whatever the outcome, instead of adding up policies on success and reading the failure block on failure.
| Field | Type | Description |
|---|---|---|
total | Integer | ACL operations attempted for this resource |
applied | Integer | ACL operations that succeeded |
failed | Integer | ACL operations that failed. Any value above 0 makes this event FAILED, even when the sync completed overall |
skipped | Integer | ACL operations that were skipped rather than attempted |
Absent when the resource did not change, because no grants ran for it.
Truncated policy lists
These totals are computed across all policies in the sync, before any truncation. When policiesTruncated is present, aclsExecuted can therefore describe more policies than policies lists. That is deliberate: the counts stay accurate even when the list is shortened, so a failure inside a truncated-away policy is still visible.
Why the outcome failed¶
The output.errorDetails object explains a failure. It never appears on a successful outcome, and it does not appear on every failed one either — a resource that never reached grant application has no cause to report. When it is present, it carries only the parts that apply.
| Field | Type | Description |
|---|---|---|
topReasons | Array | Optional. The most common failure reasons for this resource, as {reason, count} objects. Present only when grants were attempted |
serviceError | String | Optional. The raw error from the target service, kept word for word |
Which of the two appear depends on the outcome — see Outcome codes. topReasons is a summary; the full per-policy breakdown stays in policies[].failedReasons. ACL counts are not here — they live in ACL totals.
Input: the original request¶
The extraInfo.input object echoes back the request this event answers, so you can match an event to its request without calling the Ops Server.
| Field | Type | Description |
|---|---|---|
createTime | Integer | When the request was created, in epoch milliseconds |
syncType | String | The request type. RESOURCE_SYNC for on-demand resource syncs |
source | String | Which Ops Server source the request arrived through, for example REST_API or MSK |
appType | String | The type of application that made the request, for example PS_CONNECTOR |
resources | Array | Holds one entry: the resource this event describes |
The source value is the OPS_BRIDGE_SOURCE name configured for the connector, so it names the path the request came in through — the REST API, MSK, or another configured source. See On-Demand Sync for PolicySync Connectors.
Each element of input.resources[]:
| Field | Type | Description |
|---|---|---|
type | String | The requested resource type, such as table, schema, or catalog |
values | Object | The resource as it was requested, such as catalog, schema, and table, plus the requestor fields below |
Inside input.resources[].values:
| Field | Type | Description |
|---|---|---|
requestor_id | String | Who requested this resource. Present when the request supplied it |
requestor_create_time | String | When this resource was requested, in epoch seconds |
These two fields are per resource, not per request. If one request asks for several resources on behalf of different people, each event carries its own requestor.
Sample event¶
One resource (table1) added by on-demand task 6, with one policy applied:
A request for three resources produces three events like this one, all sharing ondemandTaskId, changeLogId, and timeTakenMs.
Outcome codes¶
Every requested resource gets exactly one event, and every event carries extraInfo.output.errorCode. The code is the stable, machine-readable outcome — branch on errorCode rather than on msg, because the wording of msg can change between releases while the code does not.
errorCode is present on success as well as on failure, so a consumer can switch on one field instead of inferring the outcome from status plus the counts.
Where each one shows up
Privacera Portal displays the msg text in the Message column of the audit list, so that is what an operator reads. errorCode travels in the event payload rather than as a Portal column, which is what makes it the right field for a consumer reading the events from Solr, S3, Amazon SQS, or Amazon SNS. Both describe the same outcome.
errorCode | status | msg | When it happens | errorDetails |
|---|---|---|---|---|
SYNCED_GRANTS_APPLIED | COMPLETED | Resource synced; access grants applied | The resource changed and a matching policy applied its grants | — |
NO_POLICY_FOUND | COMPLETED | Resource synced; no matching policy found, so no grants were applied | The resource changed, but no policy matched it, so there was nothing to grant | — |
NO_CHANGE_DETECTED | COMPLETED | No change detected since last sync, grant not reapplied | The resource was loaded and is unchanged since the last sync, so grants were not re-applied | — |
RESOURCE_DELETED | COMPLETED | Resource deleted from source | The resource no longer exists in the source and was removed; its grants were revoked | — |
RESOURCE_NOT_FOUND | FAILED | Resource not found in source | The requested resource does not exist in the source, so nothing was synced and no grants were applied | none — it never entered grant application, so the sync's grant error is not its cause |
RESOURCE_LOAD_FAILED | FAILED | Resource load failed | The resource load itself did not complete (for example the source could not be reached, or the resource key could not be persisted) | serviceError |
GRANT_FAILED | FAILED | Failed to grant | Grant application ran for this resource and all ACL operations failed | topReasons, serviceError |
GRANT_PARTIALLY_APPLIED | FAILED | Grants partially applied; some grants failed | Grant application ran and some ACL operations succeeded while others failed | topReasons, serviceError |
Which resources a failure affects¶
errorCode | Scope within the request |
|---|---|
RESOURCE_NOT_FOUND | Only the missing resource |
RESOURCE_LOAD_FAILED | Every resource in the request, because the load did not finish |
GRANT_FAILED, GRANT_PARTIALLY_APPLIED | Only resources that changed, since only those reached grant application |
Partial grants are detected from the counts
GRANT_PARTIALLY_APPLIED is decided from output.aclsExecuted, not from the changelog status. Individual ACL operations can fail while the sync completes overall, so a resource where 4 of 6 grants applied is reported as a partial failure rather than a clean success.
Not found is not the same as deleted
A deleted resource existed, was removed from the source, and had its grants revoked — that is successful work, so RESOURCE_DELETED reports COMPLETED. A not-found resource never existed as requested, so nothing was synced at all; RESOURCE_NOT_FOUND reports FAILED so the request is visibly flagged rather than reported as a clean completion.
Requests that cannot be matched to one resource
A coarse or partial request — for example a parent path whose children are keyed individually — cannot always be matched back to a single resource's load result. Such an entry reports NO_CHANGE_DETECTED with empty resources and policies. The sync itself still ran; only the per-resource attribution is unavailable. Two consequences worth knowing when reading these events: a coarse re-sync whose child resources did change still reports "no change" for the parent entry, and such an entry stays COMPLETED even when grant application failed elsewhere in the same sync.
Failed use-cases¶
A failed outcome always emits status = FAILED, a readable msg from the table above, a machine-readable errorCode, and — where there is a cause to report — the underlying error in errorDetails.serviceError.
msg never carries the raw error
The readable msg comes only from the outcome code, so it is safe to display. The target service's own error text is preserved verbatim in errorDetails.serviceError, and per-policy failure reasons are grouped into errorDetails.topReasons. Read those for diagnosis, not msg.
Resource not found¶
output.resources and output.policies are both empty, and there is no errorDetails block — the resource never entered grant application, so the sync's grant error is not attributed to it. input still echoes the request, so you can see exactly which resource was missing.
Resource load failed¶
The load did not complete, so every resource in the task is FAILED with the same code, and output.resources is empty on all of them. The load error itself is in errorDetails.serviceError, not in msg.
Grant application partially applied¶
The resource did change, so its counts are reported and the policy list rides the FAILED event, just as it does on a COMPLETED event. Read output.aclsExecuted to see how far the grants got: here 1 of 3 ACL operations applied and 2 failed, which is a partial grant — hence GRANT_PARTIALLY_APPLIED rather than GRANT_FAILED. The target's own error text is grouped in errorDetails.topReasons and repeated per policy in policies[].failedReasons.
When every grant fails
An event where none of the ACL operations succeeded has the same shape but reports GRANT_FAILED with msg Failed to grant, and output.aclsExecuted shows applied: 0. The distinction matters when reading these events: GRANT_PARTIALLY_APPLIED means some access was granted, GRANT_FAILED means none was.
Validate¶
In Privacera Portal¶
The audits are indexed into Apache Solr, which is what Privacera Portal reads, so you can confirm the feature is working from the Portal without querying anything directly.
- Sign in to Privacera Portal.
- From the application menu at the top, select Access Controls.
- In the left navigation, select Audits.
- Select the Policy Sync tab.
- Adjust the date range at the top right if your sync is older than the default window, and use the search box to narrow the list.
- The tab lists every PolicySync event, so narrow it to the rows where Event Type is
ON_DEMAND.
Each row is one requested resource. Read the outcome from these columns:
| Column | What it shows |
|---|---|
| Event Type | ON_DEMAND for a requested sync |
| Status | COMPLETED or FAILED for that one resource |
| Message | The readable outcome, for example Resource not found in source |
| Ondemand Task ID | Groups every resource from the same request |
| Time Taken | How long the sync took |
To review one request end to end, note its Ondemand Task ID and look at every row sharing that value.
If a sync produced no ON_DEMAND rows at all, the audit is most likely not enabled on the connector — check CONNECTOR_ON_DEMAND_AUDIT_ENABLE.
In Prometheus¶
The connector publishes one counter for on-demand audit emission, so you can confirm events are being produced — and spot events that failed to reach the audit pipeline at all, which by definition never reach the Portal or Solr.
| Metric | Description |
|---|---|
ps_core_ondemand_audit_events_total | One increment per ON_DEMAND audit event. Tagged connector, result, status, stage, and path |
| Tag | Values | Meaning |
|---|---|---|
connector | connector instance name | Which connector produced the event |
result | emitted, failed | Whether the event reached the audit pipeline |
status | COMPLETED, FAILED, none | The audit outcome the event reports |
stage | build, send, none | Where emission broke, if it did |
path | normal, processor_construction, stats_construction | How the emission was reached |
Useful queries:
| Text Only | |
|---|---|
Any non-zero result="failed" means an audit event was produced but never delivered — worth alerting on. A non-zero path!="normal" means something upstream failed while the audit was still emitted.
path is not an outcome
A fallback emission is still counted under result="emitted". Do not treat path as an alternative to success or failure, or the same events will be counted twice.
In Amazon SQS¶
Use the built-in test script inside the Audit Server diagnostics container to confirm ON_DEMAND records are arriving in your queue.
Warning
Setting DELETE_AFTER_PROCESSING to true permanently removes messages from the queue after the script reads them. Use false to inspect messages without consuming them.
- Open a shell inside the Audit Server diagnostics container.
- Navigate to the SQS test directory:
Bash - Set the required environment variables. Replace
<account-id>and<queue-name>with your AWS account ID and queue name. - Run the script:
Bash
The script prints the messages it reads. Look for records with "eventType":"ON_DEMAND" to confirm on-demand audits are flowing to the queue.
Guidelines¶
- The
ON_DEMANDaudit is per resource — an on-demand request for N resources produces N audit records. Keep this in mind when sizing a large batch, and watch the SQS/SNS 256 KB per-message limit for very large, policy-heavy syncs. - Build consumers against
extraInfo.output.errorCode, notmsg. New codes may be added across releases. - Enabling
CONNECTOR_ON_DEMAND_AUDIT_ENABLEdoes not change the Ops Server / task-status flow; that continues to work whether the flag is on or off. - If you filter an Amazon SNS subscription by event type, allow
eventType = ON_DEMAND— otherwise the on-demand events are dropped before they reach the subscribed queue even though the connector emitted them.
Applies to
Self-Managed Privacera only. PrivaceraCloud customers should contact their Privacera representative. This configuration applies to the per-resource ON_DEMAND audit on the policysync_audit collection.