Data Events
Note
SOS Data Events for Audit-Trail is only available for customers with
Enterprise support plan and are affected by a specific extra cost.
SOS Data Events for Audit-Trail give you detailed logs for all API requests executed against the SOS service, including accepted and rejected requests, information about the querier, and the affected objects.
Events are grouped into batches of up to 1'000 events and are submitted as
JSON-encoded payloads to a webhook you provide. Calls to the webhook will
include an HMAC-SHA256 signature stored in the exo-audittrail-signature HTTP
header, computed using a key given by Exoscale during the configuration process.
Guarantees
SOS Data Events for Audit-Trail are delivered on a best-effort basis, the exhaustivity and delivery duration of events is not guaranteed. The data event can be used to understand the traffic against your bucket. Some events can be lost, duplicated or significantly delayed.
Requests against the wrong region, resulting in a redirect, do not generate any data-events and will not be visible. When the request is executed against the right zone after following the redirect, data events will be generated.
Configuration
The configuration is per-bucket and will include all data events. An HTTPS
webhook URL targeting port 443 and handling POST requests must be provided to
Exoscale support. In return you will be given a symmetric key that you can use
to validate the accompanying HMAC-SHA256 signature.
Sample output
Here is an example JSON payload received by the webhook after executing
show-object-cors followed by delete-object:
[
{
"api-key": null,
"handler": "show-object-cors",
"request-id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"request-method": "OPTIONS",
"resource": "my-bucket",
"source-ip": "1.2.3.4",
"status": 200,
"timestamp": "2026-01-01T01:01:01.123456789Z",
"uri": "https://my-bucket.sos-at-vie-1.exo.io",
"user-agent": "Mozilla/5.0 ...",
"zone": "at-vie-1"
},
{
"api-key": null,
"handler": "delete-object",
"iam-role": {
"id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
"name": "Owner"
},
"iam-user": {
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"role": {
"id": "dddddddd-dddd-dddd-dddd-dddddddddddd"
}
},
"request-id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"request-method": "DELETE",
"resource": "my-bucket",
"source-ip": "4.3.2.1",
"status": 200,
"timestamp": "2026-01-01T01:01:01.123456789Z",
"uri": "https://my-bucket.sos-at-vie-1.exo.io/my-object-name.txt",
"user-agent": "Mozilla/5.0 ...",
"zone": "at-vie-1"
}
]Here is another example payload received after performing a batch delete using
delete-objects. Specially for delete-objects, the data event includes the
body field that contains a JSON encoding of the
XML response
sent by SOS:
[
{
"api-key": "EXOaaaaaaaaaaaaaaaaaaaaaaaa",
"body": {
"DeleteResult": {
"Deleted": [
{
"Key": "mykey-0acf6939"
},
{
"Key": "mykey-8ba16fd3"
}
]
}
},
"handler": "delete-objects",
"iam-api-key": {
"key": "EXOaaaaaaaaaaaaaaaaaaaaaaaa",
"name": "my-key",
"role-id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
},
"iam-role": {
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"name": "Owner"
},
"request-id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"request-method": "POST",
"resource": "my-bucket",
"source-ip": "4.3.2.1",
"status": 200,
"timestamp": "2026-01-01T00:00Z",
"uri": "https://sos-ch-dk-2.exo.io/my-bucket",
"user-agent": "Mozilla/5.0 ...",
"zone": "ch-dk-2"
}
]If your bucket has versioning enabled you will notice that the objects are replaced with delete markers instead of being deleted outright:
[
{
...
"body": {
"DeleteResult": {
"Deleted": [
{
"Key": "mykey-e3a00bf0",
"DeleteMarker": true,
"DeleteMarkerVersionId": "1245494379674602496"
},
{
"Key": "mykey-f7e094ea",
"DeleteMarker": true,
"DeleteMarkerVersionId": "1245494379674602497"
}
]
}
},
"handler": "delete-objects",
...
}
]However, if your deletion requests target specific versions rather than bare keys, then delete markers are not used and the versions are deleted directly:
[
{
...
"body": {
"DeleteResult": {
"Deleted": [
{
"Key": "mykey-cbff453c",
"VersionId": "1245494389376027648"
},
{
"Key": "mykey-0619ee01",
"VersionId": "1245494386049945600"
}
]
}
},
"handler": "delete-objects",
...
}
]Note that delete markers themselves can be deleted:
[
{
...
"body": {
"DeleteResult": {
"Deleted": [
{
"Key": "mykey-aab0954e",
"VersionId": "X/1245496299097165824",
"DeleteMarker": true
},
{
"Key": "mykey-dd18b9ee",
"VersionId": "X/1245496299097165825",
"DeleteMarker": true
}
]
}
},
"handler": "delete-objects",
...
}
]Be aware that it’s possible for deletion to fail for some objects, for example due to configured object locks mandating a retention period, etc.:
[
{
...
"body": {
"DeleteResult": {
"Deleted": [
{
"Key": "mykey-cbff453c",
"VersionId": "1245494389376027648"
},
],
"Errors": [
{
"Key": "mykey-bbcf5de5",
"VersionId": "1245494399173922816",
"Message": "Access denied"
}
}
},
"handler": "delete-objects",
...
}
]Webhook example
As a starting point, here is an example of a webhook which can be used to receive, validate and process data events.
import json
import base64
import hashlib
import hmac
from http.server import BaseHTTPRequestHandler
....
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
signing_key = None
....
def do_POST(self):
content_length = int(self.headers.Get("Content-Length", 0))
body = self.rfile.read(content_length)
# Verify HMAC-SHA256 signature
signature_header = self.headers.get("exo-audittrail-signature")
if not signature_header:
self.send_response(400)
self.end_headers()
self.wfile.write(b"Bad Request: Missing signature header")
return
try:
# Decode the signing key from base64
key = base64.b64decode(self.signing_key)
# Compute expected signature
expected_signature = hmac.new(key, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature_header, expected_signature):
self.send_response(400)
self.end_headers()
self.wfile.write(b"Bad Request: Invalid signature")
return
except Exception as e:
self.send_response(400)
self.end_headers()
self.wfile.write(b"Bad Request: Signature verification error")
return
data = json.loads(body.decode("utf-8"))
....
self.send_response(200)
self.end_headers()