Python interface for Altair Graph Studio (AGS/Anzo) — programmatically manage graphmarts, layers, steps, and monitor infrastructure without the AGS UI.
Designed for use in Databricks notebooks, CI/CD pipelines, and DevOps automation.
- Installation
- Quick Start
- API Reference — GraphmartManagerApi
- Monitoring Hooks
- Infrastructure Monitoring
- Log-Based Monitoring
- AnzoGraph Direct Monitoring
- Elasticsearch Direct Monitoring
- Backend Sidecar Service
- Common Use Cases
- Error Handling
- Troubleshooting
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtDatabricks:
%pip install requestsOptional — remote log access via SSH:
pip install paramikofrom graphmart_manager import GraphmartManagerApi
api = GraphmartManagerApi(
server="your-anzo-server.example.com",
username="sysadmin",
password="your-password",
port="443",
https=True,
verify_ssl=False
)
status = api.status("http://example.org/graphmart/production")
print(f"Status: {status}")
api.refresh("http://example.org/graphmart/production")
api.block_until_ready("http://example.org/graphmart/production", timeout=600)GraphmartManagerApi(
server: str, # Anzo server hostname or IP
username: str,
password: str,
port: str = '8443', # Use '443' for standard HTTPS deployments
https: bool = True,
verify_ssl: bool = False, # Set True if using valid certificates
api_version: str = '', # Leave empty for Anzo 5.4.2+ (including 5.4.15+)
# Set 'v1' only for Anzo 5.4.1
)| Method | Description |
|---|---|
refresh(graphmart_uri) |
Refresh a graphmart (re-process without full reload) |
reload(graphmart_uri) |
Full reload of a graphmart |
reload_and_wait(graphmart_uri, timeout) |
Full reload, blocks until Online |
activate(graphmart_uri, azg_uri) |
Activate graphmart against a specific AZG server |
deactivate(graphmart_uri, timeout=60) |
Deactivate graphmart |
| Method | Description |
|---|---|
status(graphmart_uri) |
Current status string (e.g. "Online") |
status_details(graphmart_uri) |
Full JSON status including layer info |
is_complete(graphmart_uri) |
Whether graphmart processing is complete |
health_check(graphmart_uri) |
Logs failed/dirty layers; returns counts |
block_until_ready(graphmart_uri, timeout=1000) |
Blocks until Online and complete |
get_title(graphmart_uri) |
Human-readable graphmart title |
| Method | Description |
|---|---|
graphmart_layers(graphmart_uri) |
List all layers |
create_layer(graphmart_uri, layer_config) |
Create a new layer |
create_layer_step(layer_uri, step_config) |
Create a QueryStep in a layer |
move_layer(graphmart_uri, target, position, before) |
Reorder layers |
enable_layers(graphmart_uri, layer_uris=None, steps=True) |
Enable layers (pass None for all) |
disable_layers(graphmart_uri, layer_uris=None, steps=True) |
Disable layers |
layer_steps(layer_uri) |
List all steps in a layer |
| Method | Description |
|---|---|
enable_step(step_uri) |
Enable a step |
disable_step(step_uri) |
Disable a step |
enable_layer_steps(layer_uri, step_uris=None) |
Enable steps in a layer |
disable_layer_steps(layer_uri, step_uris=None) |
Disable steps in a layer |
get_step_details(step_uri, query) |
Get expanded step details |
update_step(step_uri, data) |
Update step configuration |
Requires Anzo 5.4.15 or later and sysadmin privileges.
| Method | Description |
|---|---|
get_inflight_queries() |
List all running queries (operationId + datasource) |
cancel_query(datasource_uri, operation_id) |
Cancel a specific query |
cancel_all_inflight_queries() |
Cancel all running queries; returns count cancelled |
queries = api.get_inflight_queries()
for q in queries:
print(f"operationId={q['operationId']} datasource={q['datasource']}")
# Cancel one
api.cancel_query(queries[0]['datasource'], queries[0]['operationId'])
# Cancel all
cancelled = api.cancel_all_inflight_queries()
print(f"Cancelled {cancelled} queries")api.restart_anzograph(
azg_uri="http://anzograph-host/datasource/AnzoGraph",
graphmart_uris=[
"http://cambridgesemantics.com/graphmart/gm1",
"http://cambridgesemantics.com/graphmart/gm2",
],
deactivate_timeout=120,
ready_timeout=1800,
)This deactivates all supplied graphmarts, sends a GqeReloadRequest to restart AnzoGraph, then reactivates and waits for each graphmart to come back online.
monitoring_hooks.py provides graphmart-level monitoring functions suitable for dashboards, CI/CD validation, and alerting.
from monitoring_hooks import check_graphmart_status, ArtifactStatus, monitor_graphmarts
api = GraphmartManagerApi(server="anzo.example.com", username="admin", password="pw")
report = check_graphmart_status(api, "http://example.org/graphmart/production")
print(f"Status: {report.overall_status.value}")
print(f"Failed layers: {report.failed_layers}")
if report.overall_status == ArtifactStatus.FAILED:
print(f"Error: {report.error_message}")| Function | Description |
|---|---|
check_graphmart_status(api, uri) |
Detailed status report |
check_graphmart_health(api, uri) |
Health check with layer error details |
get_layer_status(api, uri) |
Status of all layers in a graphmart |
check_azg_connection(api, uri) |
AZG connectivity status |
monitor_graphmarts(api, uris, interval, callback) |
Continuous monitoring with callbacks |
wait_for_graphmart_ready(api, uri, timeout) |
Returns bool when ready |
Continuous monitoring with alerting:
def on_status(report):
if report.overall_status == ArtifactStatus.FAILED:
send_slack_alert(f"{report.title} FAILED: {report.error_message}")
monitor_graphmarts(api, ["http://example.org/graphmart/gm1"], interval=300, callback=on_status)infrastructure_monitoring.py checks infrastructure health using the AGS REST API.
Indirect — inferred from layer/step failures that mention Elasticsearch:
from infrastructure_monitoring import check_elasticsearch_connectivity
report = check_elasticsearch_connectivity(api, graphmart_uri)
if not report.is_healthy:
print(f"{report.failed_es_layers} ES-related layer failures")
for err in report.error_messages:
print(f" {err}")from infrastructure_monitoring import check_anzograph_connectivity
report = check_anzograph_connectivity(api, graphmart_uri)
print(f"Connected: {report.is_connected} AZG: {report.azg_uri}")One-shot test — authenticates against the API and measures response time:
from infrastructure_monitoring import check_ldap_authentication
report = check_ldap_authentication(
server="anzo.example.com",
username="testuser",
password="testpass"
)
print(f"Authenticated: {report.is_authenticated} ({report.response_time_ms:.0f}ms)")Runs repeated authentication checks at a fixed interval over a time window. Useful for detecting transient LDAP connectivity issues:
from infrastructure_monitoring import monitor_ldap_authentication
def on_failure(report):
send_alert(f"LDAP auth failed at {report.timestamp}: {report.error_message}")
result = monitor_ldap_authentication(
server="anzo.example.com",
username="sysadmin",
password="secret",
window_seconds=3600, # Monitor for 1 hour
interval_seconds=60, # Check every minute
on_failure=on_failure, # Optional: called immediately on each failure
)
print(f"Passed {result.checks_passed}/{result.checks_performed} checks")
print(f"Avg response: {result.avg_response_time_ms:.0f}ms")
if result.checks_failed > 0:
print(f"Failures at: {result.failure_timestamps}")from infrastructure_monitoring import run_infrastructure_health_check
results = run_infrastructure_health_check(
api,
graphmart_uris=["http://example.org/graphmart/gm1"],
check_ldap=True
)
print(f"Overall: {results['overall_healthy']}")
print(f"Elasticsearch: {results['elasticsearch_healthy']}")
print(f"AnzoGraph: {results['anzograph_healthy']}")
print(f"LDAP: {results['ldap_healthy']}")log_monitoring.py scans Anzo server log files for LDAP exceptions and AnzoGraph user activity. Supports reading logs locally or remotely over SSH (pip install paramiko).
Default log path: /opt/Anzo/Server/logs/AnzoServer.log (Anzo 5.4+)
Scans logs for Java LDAP stack traces (javax.naming.*Exception, com.unboundid.ldap), bind failures, and generic LDAP error strings within a time window:
from log_monitoring import scan_ldap_exceptions
# Local logs
report = scan_ldap_exceptions(window_minutes=60)
print(f"LDAP exceptions in last hour: {report.exception_count}")
for exc in report.exceptions:
print(f" [{exc['timestamp']}] {exc['message']}")Remote logs via SSH:
report = scan_ldap_exceptions(
window_minutes=60,
ssh_host="192.168.5.96",
ssh_user="foundry",
ssh_key_path="~/.ssh/devops_id_rsa",
)Custom log paths or additional patterns:
import re
report = scan_ldap_exceptions(
window_minutes=30,
log_paths=["/opt/Anzo/Server/logs/AnzoServer.log"],
extra_patterns=[re.compile(r"my-custom-ldap-error", re.IGNORECASE)],
)Counts unique users who submitted queries to AnzoGraph within a time window by parsing log lines for username patterns:
from log_monitoring import scan_anzograph_user_activity
report = scan_anzograph_user_activity(window_minutes=120)
print(f"Unique users in last 2 hours: {report.unique_user_count}")
print(f"Total query log lines: {report.query_count}")
for user in sorted(report.users):
print(f" {user}")Remote:
report = scan_anzograph_user_activity(
window_minutes=60,
ssh_host="192.168.5.96",
ssh_user="foundry",
ssh_key_path="~/.ssh/devops_id_rsa",
)Note: Username extraction depends on your Anzo log format. If
unique_user_countis 0 but you expect activity, supplyextra_user_patternswith compiled regex patterns that match your log format (each must have one capture group for the username).
anzograph_monitoring.py talks directly to AnzoGraph's SPARQL endpoint (back-end port 7070, no authentication required) to validate reachability and measure performance independently of Anzo.
from anzograph_monitoring import (
check_anzograph_liveness,
measure_anzograph_latency,
measure_anzograph_throughput,
)
liveness = check_anzograph_liveness("azg-host.example.com")
print(f"Alive: {liveness.is_alive} ({liveness.response_time_ms:.1f}ms)")
latency = measure_anzograph_latency("azg-host.example.com", num_probes=10)
print(f"Median: {latency.median_ms:.1f}ms stdev: {latency.stdev_ms:.1f}ms")
throughput = measure_anzograph_throughput("azg-host.example.com", row_limit=10_000)
print(f"Throughput: {throughput.rows_per_second:,.0f} rows/sec")Run the example: python examples/check_anzograph.py
elasticsearch_monitoring.py validates ES connectivity by calling the Elasticsearch HTTP API directly — no Anzo involved:
from elasticsearch_monitoring import validate_elasticsearch_connectivity
report = validate_elasticsearch_connectivity(
host="es-host.example.com",
port=9200,
index_filter="anzo",
)
print(f"Reachable: {report.is_reachable}")
print(f"Cluster: {report.cluster_health.cluster_name} — {report.cluster_health.status}")
print(f"Unassigned shards: {report.cluster_health.unassigned_shards}")
for node in report.nodes.nodes:
print(f" {node.node_name}: heap {node.heap_used_pct:.1f}% OS mem {node.os_used_pct:.1f}%")
for idx in report.indices:
print(f" {'✓' if idx.is_queryable else '✗'} {idx.index}: {idx.health} {idx.doc_count:,} docs")Run the example: python examples/check_elasticsearch.py
Some metrics are not available via the AGS REST API and require a small sidecar service deployed on the Anzo server. backend_monitoring_client.py is the Python client for this service.
| Metric | Without sidecar | With sidecar |
|---|---|---|
| Network bandwidth (AZG↔Anzo) | See anzograph_monitoring.py (SPARQL timing) |
Full interface stats via psutil |
| JVM heap / GC stats | Not available | Direct via jstat |
| CPU utilization | Not available | Process-level via psutil |
| Disk I/O | Not available | Via psutil |
| Elasticsearch direct health | Via elasticsearch_monitoring.py |
Redundant (use elasticsearch_monitoring.py directly) |
| LDAP group membership | Auth-test only | Direct LDAP query |
| AnzoGraph active connections | Not available | netstat/psutil |
from backend_monitoring_client import BackendMonitoringClient
client = BackendMonitoringClient("http://anzo-server:9090")
if not client.health_check():
raise RuntimeError("Backend service not reachable")
jvm = client.get_jvm_metrics()
print(f"Heap: {jvm.heap_utilization_pct:.1f}% Full GCs: {jvm.full_gc_count}")
net = client.get_network_metrics()
print(f"RX: {net.bytes_recv / 1024 / 1024:.1f} MB TX: {net.bytes_sent / 1024 / 1024:.1f} MB")
ldap = client.check_ldap_group("jsmith", "anzo-admins")
print(f"Is member: {ldap.is_member} All groups: {ldap.all_groups}")
client.print_summary() # Human-readable summary of all metricsThe sidecar is a Python Flask service in examples/backend_service/:
# Copy to server
scp examples/backend_service/anzo_monitoring_service.py anzo-server:/opt/anzo/monitoring/
scp examples/backend_service/requirements.txt anzo-server:/opt/anzo/monitoring/
ssh anzo-server
cd /opt/anzo/monitoring
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Install as a systemd service
sudo cp examples/backend_service/anzo-monitoring.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now anzo-monitoring
sudo systemctl status anzo-monitoringThe service listens on localhost:9090 by default. Secure it with an API key if exposing beyond localhost — pass api_key to BackendMonitoringClient and set the API_KEY environment variable on the service.
The Anzo JVM process is typically owned by root on install4j deployments. The service handles this gracefully:
| Endpoint | Non-root behaviour |
|---|---|
/metrics/cpu |
Falls back to /proc/{pid}/stat and /proc/{pid}/status — world-readable, no privileges needed. Response includes "source": "/proc". |
/metrics/jvm |
Requires ptrace-attach to the JVM. Returns a hint field in the error pointing to the sudoers fix (see below). |
/metrics/network, /metrics/disk |
Host-level counters — no privileges needed. |
/metrics/anzograph |
Port-level connection counts — no privileges needed. |
Enabling JVM GC stats without full root — add a narrow sudoers rule and set JSTAT_SUDO=true:
# Find the jstat path (install4j bundles its own JRE)
find /opt/i4j_jres -name jstat
# Create sudoers rule (replace path and username as needed)
sudo tee /etc/sudoers.d/anzo-monitor << 'EOF'
Cmnd_Alias ANZO_JSTAT = /opt/i4j_jres/1.8.472/bin/jstat -gc *
anzo-monitor ALL=(root) NOPASSWD: ANZO_JSTAT
EOFThen set in the systemd unit or environment:
Environment="JSTAT_SUDO=true"
Standard Anzo 5.x installs use install4j which bundles its own JRE and names the launcher AnzoLauncher. Two environment variables must be set — add them to the systemd unit:
Environment="ANZO_PROCESS_NAME=AnzoLauncher"
Environment="JSTAT_PATH=/opt/i4j_jres/1.8.472/bin/jstat"
To find the correct jstat path on any install4j deployment: find /opt/i4j_jres -name jstat
api.restart_anzograph(
azg_uri="http://example.org/azg/lakehouse",
graphmart_uris=["http://example.org/graphmart/prod"],
)server = dbutils.secrets.get(scope="anzo", key="server")
username = dbutils.secrets.get(scope="anzo", key="username")
password = dbutils.secrets.get(scope="anzo", key="password")
api = GraphmartManagerApi(server=server, username=username, password=password)
api.refresh("http://example.org/graphmart/daily-refresh")
api.block_until_ready("http://example.org/graphmart/daily-refresh", timeout=3600)
health = api.health_check("http://example.org/graphmart/daily-refresh")
if health['failedLayers'] > 0:
raise Exception(f"Refresh completed with {health['failedLayers']} failed layers")#!/usr/bin/env python3
import sys
from graphmart_manager import GraphmartManagerApi
from monitoring_hooks import check_graphmart_status, ArtifactStatus
api = GraphmartManagerApi(server="anzo.example.com", username="admin", password="pw")
graphmarts = ["http://example.org/graphmart/gm1", "http://example.org/graphmart/gm2"]
all_healthy = True
for uri in graphmarts:
report = check_graphmart_status(api, uri)
if report.overall_status != ArtifactStatus.HEALTHY:
print(f"UNHEALTHY: {report.title} — {report.error_message}")
all_healthy = False
sys.exit(0 if all_healthy else 1)layers = api.graphmart_layers(graphmart_uri)
layer_uris = [l['uri'] for l in layers if 'optional' in l['title'].lower()]
api.disable_layers(graphmart_uri, layer_uris, steps=True)
# ... do something ...
api.enable_layers(graphmart_uri, layer_uris, steps=True)from graphmart_manager import GraphmartManagerApi, GraphmartManagerApiException
import requests
try:
api.block_until_ready(graphmart_uri, timeout=300)
except GraphmartManagerApiException as e:
# Timeout, failed layers, dirty layers, or permission denied
print(f"Graphmart error: {e}")
except requests.exceptions.HTTPError as e:
# 401, 404, 500, etc.
print(f"HTTP error: {e}")
except requests.exceptions.ConnectionError as e:
print(f"Connection error: {e}")SSL warnings — suppress when using self-signed certs:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)Connection refused / timeout
- Confirm the server is reachable and port is correct (default
8443; use443for standard HTTPS deployments) - Check firewall rules on the server
401 Unauthorized
- Verify username and password
- Confirm the user account is not locked in LDAP
SSL Certificate verify failed
- Set
verify_ssl=Falsefor self-signed certs, or install valid certs and setverify_ssl=True
get_inflight_queries returns HTTP 400
- This requires Anzo 5.4.15+. Earlier versions do not support the
/sparql/lds/endpoint for system tables.
Log monitoring returns 0 results
- Confirm log file paths with
ls /opt/Anzo/Server/logs/ - Check
window_minutesis large enough to cover the time range you expect - For SSH access, verify the key path and that the remote user can read the log files
- If usernames aren't extracted, your log format may differ — use
extra_user_patterns
Backend sidecar not responding
sudo systemctl status anzo-monitoringsudo journalctl -u anzo-monitoring -fcurl http://localhost:9090/health
| Module | Purpose |
|---|---|
graphmart_manager.py |
Core AGS REST API client (GraphmartManagerApi) |
monitoring_hooks.py |
Graphmart-level monitoring and continuous polling |
infrastructure_monitoring.py |
LDAP auth, ES/AZG connectivity via REST API |
log_monitoring.py |
LDAP exception detection and user activity from logs |
anzograph_monitoring.py |
Direct SPARQL-based AZG liveness, latency, throughput |
elasticsearch_monitoring.py |
Direct ES HTTP API health and index checks |
backend_monitoring_client.py |
Client for optional backend sidecar service |
- Python 3.10+
requests >= 2.28.0paramiko(optional, for remote log access via SSH)