Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

Query the audit log

Prev Next

Applies to VAST Cluster 5.5.

When your platform team saves protocol audit logs to VAST DataBase, every audited operation is a row in the audit log table. This guide answers the usual questions with the Python SDK: what happened recently, who accessed a bucket, and which requests failed.

Each code block is complete on its own. They read your connection settings from the environment, as in the quickstart.

Before you start

  • Ask what's audited. The audit log only has rows for the protocols and operation categories your platform team chose to audit, globally or in the view policy of the views you care about. Records are kept for a retention period, which defaults to one hour. An operation that wasn't audited, or whose record has expired, leaves no row. See concepts: the audit log.
  • Access: a key whose identity policy allows querying the audit log. See access.
  • For the bucket examples, set AUDIT_BUCKET to an S3 bucket whose operations are audited.

Look at the columns

Open the audit log with tx.audit_log() and list its columns with their types:

import vastdb

session = vastdb.connect()
with session.transaction() as tx:
    fields = tx.audit_log().columns()

for field in fields:
    print(field.name, field.type)
cluster_name string
cnode_name string
time timestamp[ns]
client_ip string
cluster_vip string
tenant string
protocol string
rpc_type string
path struct<path: string, phandle: struct<clone_id: int32, handle_id: int64>>
view_path string
name struct<name: string, phandle: struct<clone_id: int32, handle_id: int64>>
login_name string
uid int32
sid string
s3_access_keys list<item: string>
status string
nfs3_required_permissions int32
nfs3_granted_permissions int32
nfs_link_name string
nfs_link_path string
rename_path string
rename_name string
nfs_rpc_sub_types list<item: string>
nfs_symlink string
nfs4_required_access int32
nfs4_granted_access int32
nfs4_ftype string
s3_used_access_key string
s3_request_id int64
connection_type string
s3_bucket_name string
s3_multipart_upload_id int64
s3_version_id int64
s3_version_phandle struct<clone_id: int32, handle_id: int64>
s3_source_object struct<s3_bucket_name: string, name: struct<s3_object_name: string, s3_version_id: int64, s3_version_phandle: struct<clone_id: int32, handle_id: int64>, phandle: struct<clone_id: int32, handle_id: int64>>>
smb_ads_name struct<name: string, phandle: struct<clone_id: int32, handle_id: int64>>
smb_create_action string
smb_create_option int32
smb_create_disposition int32
smb_delete_on_close bool
smb_info_type string
smb_info_class string
smb_rename_struct struct<smb_ads_name: struct<name: string>, path: string>
num_ops int32
num_bytes int64
time_str string
object_type string
transaction_id int64
true_client_ip string
mtls_nfs_client_certificate_info string
link_path struct<path: string>

Each field is described in the audit log schema. Many apply to one protocol only, and are empty for the others.

What happened in the last hour

Count the operations of the last hour by protocol and operation type:

from datetime import datetime, timedelta, timezone

import vastdb
from ibis import _

since = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1)

session = vastdb.connect()
with session.transaction() as tx:
    rows = tx.audit_log().select(
        columns=["protocol", "rpc_type"],
        predicate=_.time >= since,
    ).read_all()

counts = rows.group_by(["protocol", "rpc_type"]).aggregate([("rpc_type", "count")])
for row in counts.sort_by([("rpc_type_count", "descending")]).slice(0, 15).to_pylist():
    print(f"{row['protocol']:<8} {row['rpc_type']:<28} {row['rpc_type_count']:>8,}")
S3       PUT_OBJECT                         21
S3       LIST_OBJECTS                        4
S3       GET_OBJECT                          3
S3       LIST_OBJECTS_V2                     1
S3       DELETE_OBJECT                       1

Always filter on time. The audit log grows with every audited operation, and a filter on time keeps the query to the period you care about.

Who accessed a bucket

List who read or wrote objects in a bucket in the last hour, and from where:

import os
from datetime import datetime, timedelta, timezone

import vastdb
from ibis import _

bucket = os.environ["AUDIT_BUCKET"]
since = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1)

session = vastdb.connect()
with session.transaction() as tx:
    rows = tx.audit_log().select(
        columns=["time", "client_ip", "uid", "login_name", "s3_used_access_key", "rpc_type", "status"],
        predicate=(_.time >= since)
        & (_.s3_bucket_name == bucket)
        & _.rpc_type.isin(["GET_OBJECT", "PUT_OBJECT", "DELETE_OBJECT"]),
    ).read_all()

who = rows.group_by(["uid", "login_name", "client_ip", "rpc_type"]).aggregate([("rpc_type", "count")])
for row in who.sort_by([("rpc_type_count", "descending")]).to_pylist():
    print(row["uid"], row["login_name"], row["client_ip"], row["rpc_type"], row["rpc_type_count"])
8601 cat-app 15.0.0.1 PUT_OBJECT 21
8601 cat-app 15.0.0.1 GET_OBJECT 3
8601 cat-app 15.0.0.1 DELETE_OBJECT 1

login_name is filled in only if your platform team turned on user names in the audit settings, and only when the cluster could get the name from the authentication provider. uid (NFS) and sid (SMB) identify the user otherwise; for S3, s3_used_access_key is the key that signed the request. The S3 operation types include PUT_OBJECT, GET_OBJECT, DELETE_OBJECT, LIST_OBJECTS and LIST_OBJECTS_V2.

On views that impersonate users, 5.5 records only the original user's UID, not their login name or SID (a known limitation in 5.5).

Failed requests

Find requests that didn't succeed. status is Success for a request that worked; otherwise it holds the error, such as the S3 error code NoSuchKey:

from datetime import datetime, timedelta, timezone

import vastdb
from ibis import _

since = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1)

session = vastdb.connect()
with session.transaction() as tx:
    rows = tx.audit_log().select(
        columns=["time_str", "protocol", "rpc_type", "status", "path", "client_ip"],
        predicate=(_.time >= since) & (_.status != "Success"),
        limit_rows=20,
    ).read_all()

for row in rows.sort_by("time_str").to_pylist():
    print(row["time_str"], row["protocol"], row["rpc_type"], row["status"], row["path"]["path"], row["client_ip"])
2026-10-09T16:41:42.293Z S3 GET_OBJECT NoSuchKey /cat-data/missing/no-such-object.txt 15.0.0.1

time_str is the record's time as text, which is easier to print than time: like the Catalog's times, time has nanosecond precision, finer than Python's datetime. path is a structure; its path field is the full path when your platform team turned on full paths in the audit settings.

A request that failed before the cluster could tell what it was acting on has object_type UNKNOWN. Some failed requests are recorded with only part of their fields, for example when the access key or bucket name is invalid.

Things to know

  • Use the SDK or SQL for large results. In 5.5, the VAST Web UI's audit log page (and the VMS REST API behind it) can't sort more than 1,000 matching rows, and returns 1,000 random ones instead. The SDK, Trino and Spark don't have that limit.
  • Presigned POST uploads are recorded with rpc_type PUT_OBJECT in 5.5.
  • Some operations aren't audited at all, such as VAST Event Broker produce and consume.
  • The audit log isn't replicated to another cluster.

In SQL

The same queries work through Trino or Spark; see Trino and Spark. For dashboards, see Grafana.

See also