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.

Monitoring your workload

Prev Next

Applies to VAST Cluster 5.5. Examples use the VAST DataBase Python SDK 2.1.0.

This page shows what you can see about your own queries and loads from the client: SDK logging, table statistics, sort progress, the partitions view and the imports table. Cluster-wide dashboards belong to your platform team. The last section says what they can see for you.

SDK logging

The SDK uses Python's standard logging module. Every module logs under its own name, so vastdb is the parent logger for all of them:

Logger What it logs
vastdb.features The cluster version detected when you connect (INFO).
vastdb._session_context Data endpoint discovery. Logs a WARNING if discovery only partly worked, or if the server returned no virtual IPs. In that case the SDK uses only the endpoint you connected to.
vastdb.transaction Transaction IDs as transactions open and commit (DEBUG). Logs an ERROR with the traceback when a transaction rolls back because of an exception.
vastdb.schema, vastdb.table Schemas, tables, columns and projections being created, renamed or dropped (INFO). How queries are split and which endpoints they use (DEBUG). Import batches and the endpoint each one runs on (INFO).
vastdb._by_column_inserter How insert() split a batch into requests (DEBUG).
vastdb.util The start and end of create_table_from_files() imports (INFO).
vastdb._internal Every HTTP request and response (DEBUG), retries that give up (INFO), and progress for each imported file (INFO).
vastdb.errors Failed requests: a WARNING with the error, plus the full response at DEBUG.

To see what your code is doing, set the vastdb logger to INFO:

import logging

logging.basicConfig(format="%(asctime)s %(name)s %(levelname)s %(message)s")
logging.getLogger("vastdb").setLevel(logging.INFO)

# More detail for queries and inserts only:
logging.getLogger("vastdb.table").setLevel(logging.DEBUG)

Be careful with DEBUG on vastdb._internal and vastdb.errors. Those messages include request URLs, headers and response bodies, which name your bucket, schemas and tables, and the cluster's addresses. They're useful for a support case, but don't leave them on in production logs.

Retries are logged at the level set in BackoffConfig.backoff_log_level, which is DEBUG by default. See Retries.

Set up

The examples below use the environment variables from the quickstart and a schema called ops_monitoring. Run the blocks in order in one Python session.

import os
import time
import pyarrow as pa
import vastdb

session = vastdb.connect()
BUCKET = os.environ["VASTDB_BUCKET"]

columns = pa.schema([
    ("id", pa.int64()),
    ("region", pa.utf8()),
    ("value", pa.float64()),
])
rows = pa.table({
    "id": list(range(1000)),
    "region": ["eu", "us"] * 500,
    "value": [float(i) for i in range(1000)],
}, schema=columns)

with session.transaction() as tx:
    schema = tx.bucket(BUCKET).create_schema("ops_monitoring", fail_if_exists=False)
    table = schema.create_table("events", columns, fail_if_exists=False)
    row_ids = table.insert(rows)
    print(f"Inserted {len(row_ids)} rows")
Inserted 1000 rows

Table statistics

table.get_stats() returns a TableStats object with the table's row count and size, as reported by the cluster. Each call fetches new numbers. It needs the s3:TabularGetTableStats permission (see setup prerequisites).

The statistics are kept up to date by background work, not as part of your transaction. A reading taken straight after a load can be behind, and for a while it can also include rows from transactions that haven't committed, or count deleted rows that haven't been cleaned up yet. On sorted and partitioned tables the totals can lag for minutes (write path). To check a load exactly, count the rows with a query. This example polls until the count matches:

def wait_for_row_count(table_name, expected, timeout=60):
    deadline = time.time() + timeout
    while True:
        with session.transaction() as tx:
            stats = tx.bucket(BUCKET).schema("ops_monitoring").table(table_name).get_stats()
        if stats.num_rows == expected:
            return stats
        if time.time() > deadline:
            raise TimeoutError(f"{table_name}: stats show {stats.num_rows} rows, expected {expected}")
        time.sleep(1)

stats = wait_for_row_count("events", 1000)
print("num_rows:", stats.num_rows)
print("size_in_bytes > 0:", stats.size_in_bytes > 0)
print("sorting_key_enabled:", stats.sorting_key_enabled)
print("partitioning_info:", stats.partitioning_info)
num_rows: 1000
size_in_bytes > 0: True
sorting_key_enabled: False
partitioning_info: None

Use the statistics for trends and rough checks, such as "did the nightly load land?". For an exact count at a point in time, query the table.

TableStats also has vips and endpoints: the virtual IP addresses the SDK uses to spread queries and imports across the cluster (see performance). Don't print them into shared logs. All the fields are listed in the SDK reference.

Sort progress

On a sorted table, the cluster sorts new rows in the background after they're committed. Two methods report its progress:

  • sorting_done() returns True when the background sorter has nothing left to do. That doesn't mean every row is sorted: the newest block of rows is sorted only once it's full (512K rows), so a small sorted table reports done even though none of its rows are sorted (write path). It's always False for unsorted tables.
  • sorting_score() returns a progress score. It's always 0 for unsorted tables.
with session.transaction() as tx:
    schema = tx.bucket(BUCKET).schema("ops_monitoring")
    sorted_table = schema.create_table("events_sorted", columns,
                                       fail_if_exists=False, sorting_key=["id"])
    sorted_table.insert(rows)

deadline = time.time() + 120
while True:
    with session.transaction() as tx:
        sorted_table = tx.bucket(BUCKET).schema("ops_monitoring").table("events_sorted")
        is_sorted = sorted_table.sorted_table
        done = sorted_table.sorting_done()
    if done or time.time() > deadline:
        break
    time.sleep(2)

print("sorted_table:", is_sorted)
print("sorting_done:", done)

with session.transaction() as tx:
    events = tx.bucket(BUCKET).schema("ops_monitoring").table("events")
    print("unsorted table, sorting_done:", events.sorting_done())
sorted_table: True
sorting_done: True
unsorted table, sorting_done: False

Sorting works best on tables with more than 1 million rows, because tables are sorted in chunks of 512K rows. While sorting runs, the table's row count and size can go up for a while and then come back down, but count(*) stays correct (see Sorted tables). So expect get_stats() to move around on a sorted table that's still being sorted.

The partitions view

On a partitioned table, table.partitions() returns a read-only view with one row per partition. Each row holds the partition values and two estimates, est_row_count and est_byte_size. Like the table statistics, the estimates can lag behind recent writes.

from pyiceberg.transforms import IdentityTransform
from vastdb import PartitionKey, PartitionSpec

spec = PartitionSpec([PartitionKey("region", IdentityTransform())])

with session.transaction() as tx:
    schema = tx.bucket(BUCKET).schema("ops_monitoring")
    by_region = schema.create_table("events_by_region", columns,
                                    fail_if_exists=False, partition_spec=spec)
    by_region.insert(rows)

with session.transaction() as tx:
    by_region = tx.bucket(BUCKET).schema("ops_monitoring").table("events_by_region")
    parts = by_region.partitions().select().read_all()

print(sorted(parts.schema.names))
print(sorted(parts["region"].to_pylist()))
['est_byte_size', 'est_row_count', 'region']
['eu', 'us']

Use the view to check that rows land in the partitions you expect, and to spot skew, such as one partition much larger than the rest. Only partitioned tables have it. On any other table, partitions() raises AssertionError.

The imports table

When you load Parquet files with import_files(), the table keeps a record of the files it imported. The record is a second table, the imports table, which you query with imports_table(). It needs cluster 5.2 or later.

On a 5.5 cluster, a new table already has an imports table, so you don't need to create one. Calling create_imports_table() on it raises InternalServerError with failed creating import table (res=EXIST). In SDK 2.1.0 this happens even if you pass fail_if_exists=False, because the method ignores that argument.

The example uploads two Parquet files to the same bucket with boto3, under a prefix that isn't a schema name, and then imports them:

import io
import boto3
import pyarrow.parquet as pq

s3 = boto3.client("s3", endpoint_url=os.environ["AWS_S3_ENDPOINT_URL"])
buf = io.BytesIO()
pq.write_table(rows, buf)
keys = ["ops-monitoring-files/part-0.parquet", "ops-monitoring-files/part-1.parquet"]
for key in keys:
    s3.put_object(Bucket=BUCKET, Key=key, Body=buf.getvalue())

with session.transaction() as tx:
    schema = tx.bucket(BUCKET).schema("ops_monitoring")
    schema.create_table("events_imported", columns, fail_if_exists=False)

with session.transaction() as tx:
    imported = tx.bucket(BUCKET).schema("ops_monitoring").table("events_imported")
    imported.import_files([f"/{BUCKET}/{key}" for key in keys])

with session.transaction() as tx:
    imported = tx.bucket(BUCKET).schema("ops_monitoring").table("events_imported")
    log = imported.imports_table().select(columns=["BucketName", "ObjectName"]).read_all()

print(log.schema.names)
print(sorted(log["ObjectName"].to_pylist()))
['BucketName', 'ObjectName']
['ops-monitoring-files/part-0.parquet', 'ops-monitoring-files/part-1.parquet']

The imports table also records each object's creation and modification times and size, and the start and end of each import (ImportStartTime and ImportEndTime, plus ...UTC string versions). Before you import a batch of files, you can check it against ObjectName to skip files you've already loaded. See ingest.

To follow an import while it runs, set vastdb.table and vastdb._internal to INFO. The SDK logs each batch it starts, the endpoint it uses, and each file as it finishes.

Clean up

with session.transaction() as tx:
    schema = tx.bucket(BUCKET).schema("ops_monitoring")
    for t in schema.tables():
        t.drop()
    schema.drop()

for key in keys:
    s3.delete_object(Bucket=BUCKET, Key=key)
print("Dropped ops_monitoring and its files")
Dropped ops_monitoring and its files

What your platform team can see

The cluster keeps database metrics that the SDK can't read. According to Monitoring VAST DataBases, the VAST Web UI shows:

  • On the dashboard: database bandwidth, the number of running queries, and rows scanned per second.
  • In predefined analytics reports: query latency and the number of running queries, bytes returned or read per second, rows scanned, imported, inserted, updated, deleted or filtered out per second, and requests per type (query, import, insert, update, delete).
  • Custom analytics reports built from database metrics, and capacity analytics for each database view.

If a query or load is slow and nothing on the client side looks wrong, ask for these numbers for the time it ran.

Admin task: send to your platform team. See Monitoring VAST DataBases.

See also