Skip to content

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()
Last updated on