Python API Reference¶
Reference for the Micromegas Python client: connection options, client methods, and the bundled CLI tools.
Installation¶
Install the Micromegas Python client from PyPI:
Basic Usage¶
Connection¶
import micromegas
# Connect to local Micromegas instance
client = micromegas.connect()
# Connect with dictionary encoding preservation (for memory efficiency)
client = micromegas.connect(preserve_dictionary=True)
The connect() function connects to the analytics service at a plain URI. It never reads
~/.micromegas/config.json and never authenticates the connection it returns.
The installed package version is available as micromegas.__version__.
Parameters:
- preserve_dictionary (bool, optional): Enable dictionary encoding preservation for memory-efficient data transfer. Default: False
Connecting via a named profile¶
For a remote or authenticated deployment, use connect_with_profile() instead. It reads
~/.micromegas/config.json (see Config file
(~/.micromegas/config.json) below)
and picks the auth mechanism the resolved profile names -- a static API key, OIDC, or none:
import micromegas
# Uses the "prod" profile from ~/.micromegas/config.json
client = micromegas.connect_with_profile("prod")
Simple Queries¶
import datetime
# Set up time range
now = datetime.datetime.now(datetime.timezone.utc)
begin = now - datetime.timedelta(hours=1)
end = now
# Execute query with time range
sql = "SELECT * FROM log_entries LIMIT 10;"
df = client.query(sql, begin, end)
print(df)
Client Methods¶
query(sql, begin=None, end=None)¶
Execute a SQL query and return results as a pandas DataFrame.
Parameters:
sql(str): SQL query stringbegin(datetime or str, optional): ⚡ Recommended - Start time for partition elimination. Can be adatetimeobject or RFC3339 string (e.g.,"2024-01-01T00:00:00Z")end(datetime or str, optional): ⚡ Recommended - End time for partition elimination. Can be adatetimeobject or RFC3339 string (e.g.,"2024-01-01T23:59:59Z")
Returns:
pandas.DataFrame: Query results
Performance Note:
Using begin and end parameters instead of SQL time filters allows the analytics server to eliminate entire partitions before query execution, providing significant performance improvements.
Example:
# ✅ EFFICIENT: API time range enables partition elimination
df = client.query("""
SELECT time, process_id, level, msg
FROM log_entries
WHERE level <= 3
ORDER BY time DESC
LIMIT 100;
""", begin, end) # ⭐ Time range in API parameters
# ❌ INEFFICIENT: SQL time filter scans all partitions
df = client.query("""
SELECT time, process_id, level, msg
FROM log_entries
WHERE time >= NOW() - INTERVAL '1 hour' -- Server scans ALL partitions
AND level <= 3
ORDER BY time DESC
LIMIT 100;
""") # Missing API time parameters!
# ✅ Using RFC3339 strings for time ranges
df = client.query("""
SELECT time, process_id, level, msg
FROM log_entries
WHERE level <= 3
ORDER BY time DESC
LIMIT 100;
""", "2024-01-01T00:00:00Z", "2024-01-01T23:59:59Z") # ⭐ RFC3339 strings
# ✅ OK: Query without time range (for metadata queries)
processes = client.query("SELECT process_id, exe FROM processes LIMIT 10;")
query_stream(sql, begin=None, end=None)¶
Execute a SQL query and return results as a stream of Apache Arrow RecordBatch objects. Use this for large datasets to avoid memory issues.
Parameters:
sql(str): SQL query stringbegin(datetime or str, optional): ⚡ Recommended - Start time for partition elimination. Can be adatetimeobject or RFC3339 string (e.g.,"2024-01-01T00:00:00Z")end(datetime or str, optional): ⚡ Recommended - End time for partition elimination. Can be adatetimeobject or RFC3339 string (e.g.,"2024-01-01T23:59:59Z")
Returns:
- Iterator of
pyarrow.RecordBatch: Stream of result batches
Example:
import pyarrow as pa
# Stream large dataset
sql = """
SELECT time, process_id, level, target, msg
FROM log_entries
WHERE time >= NOW() - INTERVAL '7 days'
ORDER BY time DESC;
"""
for record_batch in client.query_stream(sql, begin, end):
# record_batch is a pyarrow.RecordBatch
print(f"Batch shape: {record_batch.num_rows} x {record_batch.num_columns}")
print(f"Schema: {record_batch.schema}")
# Convert to pandas for analysis
df = record_batch.to_pandas()
# Process this batch
error_logs = df[df['level'] <= 3]
if not error_logs.empty:
print(f"Found {len(error_logs)} errors in this batch")
# Process errors...
# Memory is automatically freed after each batch
query_arrow(sql, begin=None, end=None)¶
Execute a SQL query and return results as an Apache Arrow Table. This method preserves dictionary encoding when preserve_dictionary=True is set during connection.
Parameters:
sql(str): SQL query stringbegin(datetime or str, optional): Start time for partition eliminationend(datetime or str, optional): End time for partition elimination
Returns:
pyarrow.Table: Query results as Arrow Table
Example:
# Connect with dictionary preservation
dict_client = micromegas.connect(preserve_dictionary=True)
# Get Arrow table with preserved dictionary encoding
table = dict_client.query_arrow("""
SELECT properties_to_dict(properties) as dict_props
FROM measures
""", begin, end)
# Check if column uses dictionary encoding
print(f"Dictionary encoded: {pa.types.is_dictionary(table.schema.field('dict_props').type)}")
print(f"Memory usage: {table.nbytes:,} bytes")
Dictionary Encoding for Memory Efficiency¶
For large datasets with repeated values (like properties), dictionary encoding cuts memory usage by 50-80%.
Using Dictionary-Encoded Properties¶
# Connect with dictionary preservation enabled
client = micromegas.connect(preserve_dictionary=True)
# Use properties_to_dict UDF for dictionary encoding
sql = """
SELECT
time,
process_id,
properties_to_dict(properties) as dict_props,
properties_length(properties_to_dict(properties)) as prop_count
FROM measures
WHERE time >= NOW() - INTERVAL '1 hour'
"""
# Option 1: Get as pandas DataFrame (automatic conversion)
df = client.query(sql, begin, end)
print(f"DataFrame shape: {df.shape}")
print(f"Memory usage: {df.memory_usage(deep=True).sum():,} bytes")
# Option 2: Get as Arrow Table (preserves dictionary encoding)
table = client.query_arrow(sql, begin, end)
print(f"Arrow table memory: {table.nbytes:,} bytes")
# Dictionary encoding typically uses 50-80% less memory
Compatibility with Standard Functions¶
Dictionary-encoded data works seamlessly with Micromegas UDFs:
sql = """
SELECT
-- Direct property access
property_get(properties, 'source') as source,
-- Length calculation (works with both formats)
properties_length(properties) as regular_count,
properties_length(properties_to_dict(properties)) as dict_count,
-- Convert back to array when needed
array_length(properties_to_array(properties_to_dict(properties))) as array_count
FROM measures
"""
df = client.query(sql, begin, end)
Working with Results¶
pandas DataFrames¶
All query() results are pandas DataFrames, giving you access to the full pandas ecosystem:
# Basic DataFrame operations
result = client.query("SELECT process_id, exe, start_time FROM processes;")
# Inspect the data
print(f"Shape: {result.shape}")
print(f"Columns: {result.columns.tolist()}")
print(f"Data types:\n{result.dtypes}")
# Filter and analyze
recent = result[result['start_time'] > datetime.datetime.now() - datetime.timedelta(days=1)]
print(f"Recent processes: {len(recent)}")
# Group and aggregate
by_exe = result.groupby('exe').size().sort_values(ascending=False)
print("Processes by executable:")
print(by_exe.head())
pyarrow RecordBatch¶
Streaming queries return Apache Arrow RecordBatch objects:
for batch in client.query_stream(sql, begin, end):
# RecordBatch properties
print(f"Rows: {batch.num_rows}")
print(f"Columns: {batch.num_columns}")
print(f"Schema: {batch.schema}")
# Access individual columns
time_column = batch.column('time')
level_column = batch.column('level')
# Convert to pandas (zero-copy operation)
df = batch.to_pandas()
# Convert to other formats
table = batch.to_pylist() # List of dictionaries
numpy_dict = batch.to_pydict() # Dictionary of numpy arrays
Connection Configuration¶
FlightSQLClient(uri, headers=None, preserve_dictionary=False, auth_provider=None, client_entrypoint=None)¶
For advanced connection scenarios, use the FlightSQLClient class directly:
from micromegas.flightsql.client import FlightSQLClient
# Recommended: Connect with OIDC authentication (automatic token refresh)
from micromegas.auth import OidcAuthProvider
auth = OidcAuthProvider.from_file("~/.micromegas/tokens.json")
client = FlightSQLClient(
"grpc+tls://remote-server:50051",
auth_provider=auth
)
# Connect with a static analytics API key
from micromegas.auth import StaticTokenAuthProvider
auth = StaticTokenAuthProvider.from_file("~/.micromegas/local.key")
client = FlightSQLClient(
"grpc+tls://remote-server:50051",
auth_provider=auth
)
# Connect with dictionary preservation for memory efficiency
client = FlightSQLClient(
"grpc://localhost:50051",
preserve_dictionary=True
)
# Connect to local server (equivalent to micromegas.connect())
client = FlightSQLClient("grpc://localhost:50051")
Parameters:
- uri (str): FlightSQL server URI. Use grpc:// for unencrypted or grpc+tls:// for TLS connections
- headers (dict, optional): Deprecated. Use auth_provider instead. Static headers for authentication. This parameter is deprecated because it doesn't support automatic token refresh
- preserve_dictionary (bool, optional): When True, preserve dictionary encoding in Arrow arrays for memory efficiency. Useful when using dictionary-encoded UDFs. Defaults to False
- auth_provider (optional): Recommended. Authentication provider that implements get_token() method. When provided, tokens are automatically refreshed before each request. Example: OidcAuthProvider (browser/PKCE with refresh) or StaticTokenAuthProvider (a pre-minted analytics API key, sent verbatim). This is the recommended way to handle authentication
- client_entrypoint (str, optional): Explicit label for how this client was invoked (e.g. "cli-query", the label micromegas-query passes). When omitted, the entrypoint is auto-detected. Raises ValueError if the value isn't a safe gRPC header value (printable ASCII, 64 chars or fewer). See Client Attribution below.
See the Authentication Guide for setting up OIDC authentication.
Client Attribution¶
Every query sends three additional headers, resolved once per FlightSQLClient instance and
recorded in the server's query audit log:
x-client-agent— who is driving the client. Auto-detected from known agent-harness marker environment variables (currently just Claude Code'sCLAUDECODE, reported asclaude-code);"none"when no known harness is detected. Override withMICROMEGAS_CLIENT_AGENT.x-client-entrypoint— how the client was invoked:"script","jupyter", or"repl", auto-detected from the interpreter's own state (sys.modules,sys.argv,sys.flags). The CLI passes the explicit label"cli-query"instead of relying on auto-detection. Override with theclient_entrypointconstructor parameter (raisesValueErroron an unsafe value) orMICROMEGAS_CLIENT_ENTRYPOINT(silently falls back to the detected value on an unsafe value).x-client-session— an opaque id correlating every query issued through one client instance. A fresh UUID perFlightSQLClientinstance, unless a known agent harness's session environment variable is present (currently just Claude Code'sCLAUDE_CODE_SESSION_ID), in which case that value is reused verbatim so multipleFlightSQLClientinstances within the same agent session correlate (this is what letsmicromegas-query, which builds a fresh client per invocation, still correlate queries from the same agent session).
These are analytics-only signals — never used for authentication, quota, or rate limiting —
and, like the existing x-client-type header, trivially spoofable or omittable by the caller.
MICROMEGAS_CLIENT_AGENT/MICROMEGAS_CLIENT_ENTRYPOINT overrides must be printable ASCII, 64
characters or fewer; an invalid override is silently ignored in favor of the auto-detected value
(unlike the client_entrypoint constructor parameter, which raises ValueError instead).
Of the two top-level connect functions, plain micromegas.connect() does not expose a
client_entrypoint parameter — auto-detection always runs for it. micromegas.connect_with_profile()
does take it, forwarded on every auth branch. For an explicit label, use
connect_with_profile(), construct FlightSQLClient directly, or use
oidc_connection.connect()/the now-aliased cli.connection.connect(), all of which take
client_entrypoint.
Static Analytics API Keys¶
A static analytics API key is minted server-side — via POST /api/analytics-api-keys or the
Admin page — and travels verbatim as the bearer token on every request; there is no exchange or
wrapping. Store it in a file readable only by you (chmod 0600) and load it with
StaticTokenAuthProvider.from_file(...):
from micromegas.auth import StaticTokenAuthProvider
from micromegas.flightsql.client import FlightSQLClient
auth = StaticTokenAuthProvider.from_file("~/.micromegas/local.key")
client = FlightSQLClient("grpc+tls://remote-server:50051", auth_provider=auth)
See Minting an analytics key over HTTP for how to mint a key.
Schema Discovery¶
prepare_statement(sql)¶
Get query schema information without executing the query:
# Prepare statement to discover schema
stmt = client.prepare_statement(
"SELECT time, level, msg FROM log_entries WHERE level <= 3"
)
# Inspect the schema
print("Query result schema:")
for field in stmt.dataset_schema:
print(f" {field.name}: {field.type}")
# Output:
# time: timestamp[ns]
# level: int32
# msg: string
# The query is also available
print(f"Query: {stmt.query}")
prepared_statement_stream(statement)¶
Execute a prepared statement (mainly useful after schema inspection):
# Execute the prepared statement
for batch in client.prepared_statement_stream(stmt):
df = batch.to_pandas()
print(f"Received {len(df)} rows")
Note: Prepared statements are primarily for schema discovery. Execution offers no performance benefit over query_stream().
Process and Stream Discovery¶
find_process(process_id)¶
Find detailed information about a specific process:
# Find process by ID
process_info = client.find_process('550e8400-e29b-41d4-a716-446655440000')
if not process_info.empty:
print(f"Process: {process_info['exe'].iloc[0]}")
print(f"Started: {process_info['start_time'].iloc[0]}")
print(f"Computer: {process_info['computer'].iloc[0]}")
else:
print("Process not found")
query_streams(begin, end, limit, process_id=None, tag_filter=None)¶
Query event streams with filtering:
# Query all streams from the last hour
end = datetime.datetime.now(datetime.timezone.utc)
begin = end - datetime.timedelta(hours=1)
streams = client.query_streams(begin, end, limit=100)
# Filter by process
process_streams = client.query_streams(
begin, end,
limit=50,
process_id='550e8400-e29b-41d4-a716-446655440000'
)
# Filter by stream tag
log_streams = client.query_streams(
begin, end,
limit=20,
tag_filter='log'
)
print(f"Found {len(streams)} total streams")
print(f"Streams per process:\n{streams['process_id'].value_counts()}")
query_blocks(begin, end, limit, stream_id)¶
Query data blocks within a stream (for low-level inspection):
# First find a stream
streams = client.query_streams(begin, end, limit=1)
if not streams.empty:
stream_id = streams['stream_id'].iloc[0]
# Query blocks in that stream
blocks = client.query_blocks(begin, end, 100, stream_id)
print(f"Found {len(blocks)} blocks")
print(f"Total objects: {blocks['nb_objects'].sum()}")
print(f"Total size: {blocks['payload_size'].sum()} bytes")
query_spans(begin, end, limit, stream_id)¶
Query execution spans for performance analysis:
# Query spans for detailed performance analysis
spans = client.query_spans(begin, end, 1000, stream_id)
# Find slowest operations
slow_spans = spans.nlargest(10, 'duration')
print("Slowest operations:")
for _, span in slow_spans.iterrows():
duration_ms = span['duration'] / 1000000 # Convert nanoseconds to milliseconds
print(f" {span['name']}: {duration_ms:.2f}ms")
# Analyze span hierarchy (root spans have depth 0)
root_spans = spans[spans['depth'] == 0]
print(f"Found {len(root_spans)} root operations")
Data Management¶
bulk_ingest(table_name, table)¶
Requires admin
Only callable by a caller whose resolved local-group membership includes the reserved
admins group (or a --disable-auth deployment) — see
Groups and Admin SQL Functions.
An API-key credential (both ingestion_api_keys and analytics_api_keys) normally
carries no email and so can never match a user:/group: member of admins — except
while admins still holds its seeded wildcard (*) member, in which case every
authenticated caller, API keys included, is admin until an operator adds a user:
member and removes * (see Groups's upgrade path).
Bulk ingest metadata for replication or administrative tasks. table is a
pyarrow.Table whose schema matches the target table exactly; complex
columns (struct, list, binary) are passed through natively.
import pyarrow as pa
# Example: Replicate process metadata
processes = pa.table({
'process_id': ['550e8400-e29b-41d4-a716-446655440000'],
'exe': ['/usr/bin/myapp'],
'username': ['user'],
'realname': ['User Name'],
'computer': ['hostname'],
'distro': ['Ubuntu 22.04'],
'cpu_brand': ['Intel Core i7'],
'tsc_frequency': [2400000000],
'start_time': [datetime.datetime.now(datetime.timezone.utc)],
'start_ticks': [1234567890],
'insert_time': [datetime.datetime.now(datetime.timezone.utc)],
'parent_process_id': [''],
'properties': [[]],
'audience': ['public'],
})
# Ingest process metadata
result = client.bulk_ingest('processes', processes)
if result:
print(f"Ingested {result.record_count} process records")
streams and blocks need the same column, alongside their other required fields:
streams = pa.table({
'stream_id': ['...'],
'process_id': ['550e8400-e29b-41d4-a716-446655440000'],
'dependencies_metadata': [b''],
'objects_metadata': [b''],
'tags': [[]],
'properties': [[]],
'insert_time': [datetime.datetime.now(datetime.timezone.utc)],
'format': ['micromegas-transit'],
'audience': ['public'],
})
blocks = pa.table({
'block_id': ['...'],
'stream_id': ['...'],
'process_id': ['550e8400-e29b-41d4-a716-446655440000'],
'begin_time': [datetime.datetime.now(datetime.timezone.utc)],
'begin_ticks': [0],
'end_time': [datetime.datetime.now(datetime.timezone.utc)],
'end_ticks': [0],
'nb_objects': [0],
'object_offset': [0],
'payload_size': [0],
'insert_time': [datetime.datetime.now(datetime.timezone.utc)],
'audience': ['public'],
})
Supported tables: processes, streams, blocks, payloads
Note: This method is for metadata replication and administrative tasks. Use the telemetry ingestion service HTTP API for normal data ingestion.
materialize_partitions(view_set_name, begin, end, partition_delta_seconds)¶
Requires admin
Only callable by an authenticated admin; not audience-filtered — an admin acts across every audience. See Admin SQL Functions.
Create materialized partitions for performance optimization:
# Materialize hourly partitions for the last 24 hours
end = datetime.datetime.now(datetime.timezone.utc)
begin = end - datetime.timedelta(days=1)
client.materialize_partitions(
'log_entries',
begin,
end,
3600 # 1-hour partitions
)
# Prints progress messages for each materialized partition
regenerate_partitions(view_set_name, begin, end, partition_delta_seconds)¶
Requires admin
Only callable by an authenticated admin; not audience-filtered — an admin acts across every audience. See Admin SQL Functions.
Force-regenerate existing partition(s) directly from source data, bypassing the "already up to date" freshness check materialize_partitions() stops at. Useful for rebuilding a partition whose content is unchanged but whose internal row order needs to be refreshed (e.g. an existing merged blocks partition materialized before ordered merges were introduced, or a SqlBatchView such as log_stats whose live partitions predate it declaring a merge sort order):
# Regenerate yesterday's daily blocks partition
end = datetime.datetime.now(datetime.timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
begin = end - datetime.timedelta(days=1)
client.regenerate_partitions(
'blocks',
begin,
end,
86400 # must exactly match the existing daily partition's boundaries
)
# Prints progress messages for the regenerated partition
Warning: (begin, end, partition_delta_seconds) must exactly cover an existing partition's boundaries, or the call fails loudly instead of silently creating a duplicate partition. This is an admin/rollout tool -- run calls serially, never with overlapping ranges in flight concurrently.
For a SqlBatchView, the bucket size is dictated by the existing partition's boundaries, not freely choosable: an already-large, merged partition can only be regenerated as one equally large bucket, and its extract query's applied sort order (for a view declaring with_merge_sort_order) then sorts that whole bucket's aggregated output in a single blocking pass. There is no smaller partition_delta_seconds that avoids this once a partition has already grown large -- retire it (retire_partitions) and re-materialize at a smaller delta instead.
retire_partitions(view_set_name, view_instance_id, begin, end)¶
Requires admin
Only callable by an authenticated admin; not audience-filtered — an admin acts across every audience. See Admin SQL Functions.
Remove materialized partitions to free up storage:
# Retire old partitions
client.retire_partitions(
'log_entries',
'process-123-456',
begin,
end
)
# Prints status messages as partitions are retired
Warning: This operation cannot be undone. Retired partitions must be re-materialized if needed.
Administrative Functions¶
The micromegas.admin module provides administrative functions for schema evolution and partition lifecycle management. These functions are intended for system administrators and should be used with caution.
list_incompatible_partitions(client, view_set_name=None)¶
Lists partitions with schemas incompatible with current view set schemas. Returns one row per partition.
import micromegas
import micromegas.admin
client = micromegas.connect()
incompatible = micromegas.admin.list_incompatible_partitions(client)
print(f"Found {len(incompatible)} incompatible partitions")
print(f"Total size: {incompatible['file_size'].sum() / (1024**3):.2f} GB")
Returns: view_set_name, view_instance_id, begin_insert_time, end_insert_time, incompatible_schema_hash, current_schema_hash, file_path, file_size
retire_incompatible_partitions(client, view_set_name=None)¶
Requires admin
Internally calls retire_partition_by_metadata(), which requires an authenticated admin; not audience-filtered. See Admin SQL Functions.
Retires partitions with incompatible schemas using metadata-based retirement (works for empty and non-empty partitions).
# Preview first
preview = micromegas.admin.list_incompatible_partitions(client, 'log_entries')
print(f"Would retire {len(preview)} partitions")
# Retire
result = micromegas.admin.retire_incompatible_partitions(client, 'log_entries')
print(f"Retired {result['partitions_retired'].sum()} partitions")
Returns: view_set_name, view_instance_id, partitions_retired, partitions_failed, storage_freed_bytes, retirement_messages
⚠️ DESTRUCTIVE OPERATION: Irreversible. Always preview first.
WebClient — self-service mint¶
micromegas.web_client.WebClient is the HTTP client micromegas-setup-telemetry (and every other
analytics-web-srv-facing CLI — -grants, -groups, -screens) is built on, talking to
analytics-web-srv's REST API over Bearer auth. Two methods back the setup script:
mint_ingestion_api_key(name, audience=None)—POST {base_path}/api/ingestion-api-keys. Mints a fresh key, generated server-side. On a409with{"code": "CLAIM_CONTENDED"}— transient advisory-lock contention with another concurrent claim of the same brand-new audience, not a denial — retries the same request exactly once before raising; any other non-OK status (including a secondCLAIM_CONTENDED) raisesRuntimeErrorthe same way every otherWebClientmethod does. Returns the mint response dict, including the one-time cleartextkey.my_audiences()—GET {base_path}/api/audience-grants/my-audiences. Caller-scoped, no admin access required. Returns{"is_admin", "audiences", "mint_prefix", "email", "held_pairs"}— the audiences whosemintselector matches the caller today, the caller's own admin flag, a caller-derived namespace prefix a name is minted under via--user-audience(identical composition for an admin and a non-admin caller —--audiencenever applies it, minting under the name it is given verbatim), the caller's own email, andheld_pairs: the"{audience}:{axis}"pairs the caller holds via an identity selector ("*"excluded), used to tell an audience the caller personally holds a grant on from one they can merely see via a wildcard grant.
from micromegas.web_client import WebClient
client = WebClient("https://analytics.example.com", auth_provider=auth_provider)
info = client.my_audiences()
result = client.mint_ingestion_api_key("my-laptop", audience=info["audiences"][0])
print(result["key"]) # cleartext key, returned exactly once
Command-Line Interface¶
The Micromegas Python client includes CLI tools for quick queries and administrative tasks.
micromegas-query - Run SQL Queries¶
Execute arbitrary SQL queries against the analytics service from the command line.
Usage:
Arguments:
- sql (positional, optional): SQL query to execute (omit when using --file)
Options:
- --file: Read SQL from a file path, or use - to read from stdin
- --begin: Begin timestamp (RFC 3339 like 2024-01-01T00:00:00Z, or relative like 1h, 30m, 7d). Required unless --all is used
- --end: End timestamp (RFC 3339 like 2024-01-01T00:00:00Z, or relative like 1h, 30m, 7d). Default: now
- --all: Query the entire time range (mutually exclusive with --begin/--end)
- --format: Output format - table (default), csv, or json
- --max-colwidth: Maximum column width for table format (default: 50, use 0 for unlimited)
- --profile: Named connection profile from ~/.micromegas/config.json (see Named profiles below)
- --version: Print the installed micromegas package version and interpreter, then exit
Examples:
# Query with relative time range (last hour)
micromegas-query "SELECT * FROM processes LIMIT 10" --begin 1h
# Query with relative time range (last 24 hours)
micromegas-query "SELECT * FROM log_entries LIMIT 100" --begin 24h
# Query with specific timestamps
micromegas-query "SELECT * FROM measures LIMIT 50" \
--begin 2024-01-01T00:00:00Z --end 2024-01-02T00:00:00Z
# Read SQL from a file (avoids shell quoting issues with JSONPath)
micromegas-query --file query.sql --begin 1h
# Read SQL from stdin
echo "SELECT 1" | micromegas-query --file - --all
# Output as CSV for piping to other tools
micromegas-query "SELECT * FROM list_partitions()" --all --format csv
# Output as JSON
micromegas-query "SELECT view_set_name, num_rows FROM list_partitions()" --all --format json
Configuration:
The CLI resolves connection settings from four sources, in this order:
- Profile selection, if the config file has a
profilesmap (--profile>MICROMEGAS_PROFILE>default_profile) — see Named profiles below - Environment variables (highest priority for each individual setting, applied on top of the selected profile — or the flat config file if there's no
profilesmap) - Config file:
~/.micromegas/config.json(the flat file, or the selected profile's entry) - Built-in defaults (e.g.,
grpc://localhost:50051)
Each setting is resolved independently once a profile (if any) is selected, so you can put stable values in the config file and override individual settings via env vars (e.g., for CI). On a machine with no profiles map, this is the whole story and per-field env vars are all you need to switch environments. Once a profiles map exists, though, profile selection happens first: an env-var-only invocation (e.g. MICROMEGAS_ANALYTICS_URI=... micromegas-query ... in CI) fails with a usage error unless --profile, MICROMEGAS_PROFILE, or default_profile also picks a profile — per-field env vars only override settings within the selected profile, they don't substitute for selecting one.
Environment Variables:
| Variable | Description | Default |
|---|---|---|
MICROMEGAS_ANALYTICS_URI |
FlightSQL server URI | grpc://localhost:50051 |
MICROMEGAS_OIDC_ISSUER |
OIDC issuer URL (enables OIDC auth when set with _CLIENT_ID) |
— |
MICROMEGAS_OIDC_CLIENT_ID |
OAuth client ID | — |
MICROMEGAS_OIDC_CLIENT_SECRET |
OAuth client secret (optional; required by some IdPs) | — |
MICROMEGAS_OIDC_AUDIENCE |
API audience/identifier (e.g., for Auth0, Azure) | — |
MICROMEGAS_OIDC_SCOPE |
Custom OAuth scopes | openid email profile offline_access |
MICROMEGAS_PROFILE |
Named profile to select from the config file's profiles map (see Named profiles below) |
— |
MICROMEGAS_CLIENT_AGENT |
Override for the auto-detected x-client-agent value (see Client Attribution above) |
auto-detected |
MICROMEGAS_CLIENT_ENTRYPOINT |
Override for the auto-detected x-client-entrypoint value (see Client Attribution above); the CLI always passes "cli-query" regardless of this var |
auto-detected |
Config file (~/.micromegas/config.json)¶
A small JSON file you can drop in your home directory to avoid setting env vars. Every key is optional, and any env var with the same role overrides it.
{
"uri": "grpc+tls://analytics.example.com:50051",
"client_id": "your-app-client-id",
"issuers": [
{
"issuer": "https://accounts.example.com",
"audience": "your-api-audience"
}
]
}
| Key | Maps to | Notes |
|---|---|---|
uri |
MICROMEGAS_ANALYTICS_URI |
FlightSQL server URI |
client_id |
MICROMEGAS_OIDC_CLIENT_ID |
OAuth client ID |
issuers[0].issuer |
MICROMEGAS_OIDC_ISSUER |
First issuer entry is used |
issuers[0].audience |
MICROMEGAS_OIDC_AUDIENCE |
First issuer entry is used |
api_key_file |
— (no env var; see Named profiles below) | Path to a static analytics API key file |
There are three auth mechanisms, checked in this order: a static API key, when api_key_file
resolves; OIDC, when both an issuer and a client ID are resolved (from any source); otherwise the
CLI connects without auth. A profile (or flat config) that resolves both api_key_file and a
complete OIDC pair is a configuration error — see Named profiles below.
api_key_file is a FlightSQL credential: only micromegas-query, micromegas-views, and
connect_with_profile() honor it, because the analytics web API validates OIDC tokens only, not
a static key. Every WebClient-based CLI (micromegas-screens, micromegas-grants, -groups,
-setup-telemetry) resolves auth through the shared web_auth.resolve_web_auth()
helper, which only ever branches on the OIDC fields. Of those, only micromegas-screens reports a
profile that resolves api_key_file and nothing else as an error; micromegas-grants, -groups,
and -setup-telemetry still silently connect unauthenticated on that path.
Named profiles:
The config file can hold more than one named connection — prod/dev/local, for instance — under an
optional profiles map, selected by --profile or MICROMEGAS_PROFILE:
{
"default_profile": "prod",
"profiles": {
"prod": {
"uri": "grpc://analytics.example.com:50051",
"client_id": "...",
"issuers": [{ "issuer": "https://issuer.example.com/v2.0", "audience": "..." }]
},
"dev": {
"uri": "grpc://analytics-dev.example.com:50051",
"client_id": "...",
"issuers": [{ "issuer": "https://issuer.example.com/v2.0", "audience": "..." }]
},
"ci": {
"uri": "grpc+tls://analytics.example.com:50051",
"api_key_file": "~/.micromegas/ci.key"
},
"local": { "uri": "grpc://localhost:50051" }
}
}
Each entry under profiles has exactly the shape of the flat config above (uri, client_id,
issuers, api_key_file). The flat shape shown earlier is still fully supported — omit
profiles entirely and the whole file is used as a single connection, exactly as before.
A profile (or the flat config) must use exactly one auth mechanism. Naming api_key_file
alongside a complete OIDC pair (an issuers/client_id pair, from the profile or from
MICROMEGAS_OIDC_ISSUER/MICROMEGAS_OIDC_CLIENT_ID) is a configuration error, not a precedence
rule — connect_with_profile() and the CLI both raise before attempting either auth mechanism,
naming the real source of each side, e.g.:
profile 'prod' resolves two auth mechanisms: a static API key (profile key 'api_key_file')
and OIDC (issuer from MICROMEGAS_OIDC_ISSUER, client_id from profile key 'client_id').
A profile must use exactly one -- remove 'api_key_file', or unset the OIDC settings.
Selection precedence, once a profiles map is present: --profile (flag) > MICROMEGAS_PROFILE
(env var) > default_profile (config key). There is no implicit selection, even with a single
profile defined — a profiles map always requires a selected profile, so set default_profile
even for a single-profile config. If none of --profile/MICROMEGAS_PROFILE/default_profile
resolves to a profile, or the resolved name isn't one of the keys under profiles, the CLI exits
with a usage error listing the available profile names rather than guessing.
Don't mix flat keys with a profiles map
Once a profiles map is present, it takes over completely: any top-level
uri/client_id/issuers/api_key_file left in the same file are ignored in favor of the
selected profile's values. If
you're migrating from a flat config to profiles, move those values into a profile entry
rather than leaving them at the top level — they'd otherwise become dead config with no error
or warning. Similarly, default_profile has no effect at all unless a profiles map is also
present.
Adding a profiles map also moves the OIDC token cache from the single ~/.micromegas/tokens.json
to a per-profile ~/.micromegas/tokens-<profile>.json, so switching profiles never reuses another
profile's cached token. This means turning profiles on forces one fresh login even for an
otherwise-unchanged connection — rename your existing tokens.json to the new profile's
tokens-<profile>.json path beforehand to avoid it.
micromegas-logout¶
Clears cached OIDC authentication tokens. A bare invocation clears every cached token file — the
plain ~/.micromegas/tokens.json plus every tokens-<profile>.json — so it always means "log out
of everything":
Pass --profile to narrow this to just one profile's cached token, leaving every other token file
(including the plain tokens.json) untouched:
micromegas-logout doesn't read config.json and doesn't look at MICROMEGAS_PROFILE — --profile
is its only narrowing mechanism.
A static-key profile (api_key_file) caches no token, so micromegas-logout is a no-op for it —
there's no tokens-<profile>.json to clear. Revoking such a key is a server-side operation:
DELETE /api/analytics-api-keys/{key_id}.
Pass --version to print the installed package and interpreter version and exit.
micromegas-grants¶
Creates and deletes DB-backed audience grants (audience_grants table) via
analytics-web-srv's /api/audience-grants routes — never direct Postgres access. These two write
routes are not admin-only: a non-admin caller with a matching hold on the pair can share it too,
once MICROMEGAS_SELF_SERVICE_MINT is on (see
Authorization → the grant store).
micromegas-grants --url https://analytics.example.com create team-alpha read group:eng
micromegas-grants --url https://analytics.example.com delete team-alpha read group:eng
--url always points at analytics-web-srv's base URL. Two subcommands:
create <audience> <axis> <selector>— creates (or reports the pre-existing) grant row.<axis>isreadormint;<selector>is*,user:<id>, orgroup:<id>.delete <audience> <axis> <selector>— deletes one grant row, keyed by its natural triple.
There is no list subcommand. Listing goes through the caller-scoped
list_audience_grants() SQL function instead — a non-admin gets their own scoped view (every
grant on a pair they hold), an admin gets every row, filterable and orderable like any other
table:
Auth follows the same OIDC setup as micromegas-query/-screens
(MICROMEGAS_OIDC_* for a non-interactive run, or --profile for an interactive/cached login).
Pass --version to print the installed package and interpreter version and exit.
micromegas-groups¶
Manages local group membership (groups/group_members tables) via analytics-web-srv's
/api/groups routes — never direct Postgres access. Every subcommand is admin-only — see
Groups for the full model, including the reserved admins group.
micromegas-groups --url https://analytics.example.com list
micromegas-groups --url https://analytics.example.com create eng --description "Engineering"
micromegas-groups --url https://analytics.example.com members admins
micromegas-groups --url https://analytics.example.com add admins user:alice@example.com
micromegas-groups --url https://analytics.example.com remove admins '*'
micromegas-groups --url https://analytics.example.com delete eng
--url always points at analytics-web-srv's base URL. Six subcommands:
list— every group with its member count.create <name> [--description TEXT]— creates a new, empty group.delete <name>— deletes a group (fails, 409, onadminsor while still referenced by a nested membership or an audience grant).members <name>— a group's direct members.add <name> <member>— adds*,user:<id>, orgroup:<id>to a group.remove <name> <member>— removes a member from a group.
There is no bootstrap convenience command. Taking over admin access from a wildcard-seeded
admins group (the state a fresh install or an unmigrated upgrade seeds — see
Groups → Upgrade path) is the two-command sequence:
micromegas-groups --url https://analytics.example.com add admins user:<you>
micromegas-groups --url https://analytics.example.com remove admins '*'
Auth follows the same OIDC setup as micromegas-query/-screens/-grants
(MICROMEGAS_OIDC_* for a non-interactive run, or --profile for an interactive/cached login).
Pass --version to print the installed package and interpreter version and exit.
micromegas-setup-telemetry¶
Mints a personal ingestion_api_keys key for the caller and prints the OTLP exporter env vars
needed to send that caller's own telemetry to the deployment. Named for what it does from the
user's point of view ("set up telemetry"), not the server-side term ("ingestion"). Requires
self-service mint to be enabled on the target deployment (MICROMEGAS_SELF_SERVICE_MINT; see
Self-service mint)
unless the caller is an admin.
# Resolve the audience automatically via GET .../audience-grants/my-audiences, from the
# caller's personally held mint grants only (exactly one match is used silently; more
# than one asks you to pick with --audience).
eval "$(micromegas-setup-telemetry --url https://analytics.example.com --name my-laptop)"
# An audience you already have a grant for -- e.g. the deployment's shared `public`
# audience, mintable by every authenticated caller once the operator has granted it:
micromegas-setup-telemetry --url https://analytics.example.com --name my-laptop \
--audience public
# A fresh audience of your own: --user-audience composes the name under a prefix
# derived server-side from your email -- the identical command works whether the
# caller is an admin or not.
micromegas-setup-telemetry --url https://analytics.example.com --name ci-runner \
--user-audience ci-runner --env-file ~/.micromegas/telemetry.env
# From a PowerShell prompt (Windows or pwsh on Linux/macOS) -- --format is never
# inferred from the OS, so pass it explicitly:
micromegas-setup-telemetry --url https://analytics.example.com --name my-laptop `
--format powershell | Invoke-Expression
# For a container/CI env-file loader (a compose service's env_file:, docker run
# --env-file, python-dotenv, ...):
micromegas-setup-telemetry --url https://analytics.example.com --name ci-runner \
--format dotenv --env-file .env
--url (required) is analytics-web-srv's base URL. --name (required) names the minted key
(e.g. a hostname). --user-audience and --audience are mutually exclusive, and mean two
different things:
--user-audience SUFFIX(recommended): mints underf"{mint_prefix}{SUFFIX}", wheremint_prefixis derived server-side from the caller's own email and is composed identically for an admin and a non-admin caller (e.g.alice@example.com→alice-, so--user-audience ci-runnerresolves toalice-ci-runner). Lazily claims the audience if it's genuinely fresh, writing the caller's ownread/mintgrant in the same request; if it already exists and the caller holds no grant for it (someone else's namespace), the route's ordinary 403 applies, admin included. Note thatmint_prefixis derived from the email's local part only and isn't guaranteed unique, so two callers with the same local part on different domains can share a prefix and genuinely hit this 403 under what looks like "your own" prefix. Requires a caller whose email yields amint_prefix; errors locally otherwise, with distinct messages for "no email at all" (ask an admin for a grant) vs. "email sanitizes to empty" (use--audience <name>instead).--audience NAME: mints underNAMEverbatim, unconditionally — no client-side check of the caller's mintable set. A genuinely fresh name is lazily claimed by the mint route itself, writing the caller's ownread/mintgrant in the same request (the printed mint line addsclaimed audience <name>when it did); a name someone else already holds is refused with the route's ordinary 403, admin included, which the CLI enriches with the caller's mintable audiences, a--user-audiencesuggestion, and the exactmicromegas-grantscommands an admin would run to grant this one (concretely, with the audience and the caller's email already substituted). Use this for an org/team/service audience that isn't namespaced under any one caller.- Omitted entirely: resolved via
GET .../audience-grants/my-audiences, filtered to audiences the caller personally holds a mint grant on (the response'sheld_pairs) — a deployment-wide wildcard grant that puts an audience in every caller'saudienceslist (e.g. a seededpublicmint row) is not enough on its own to be silently auto-selected here. Exactly one personally-held match is used silently; more than one prints the choices and asks for--audience; none prints the audiences the caller can see but does not personally hold (if any), plus a hint to--user-audiencea fresh name or ask an admin. Admin and non-admin alike:is_admingrants no audience of its own, so an admin with no held mint audience gets the same "none" error as anyone else, and one with exactly one held mint audience resolves it silently just like a non-admin would.
--otlp-endpoint defaults to f"{MICROMEGAS_TELEMETRY_URL}/ingestion/otlp" when that env var is
set (the repo's established ingestion-endpoint convention — see OTLP); it is a
required flag only when that env var is unset. --env-file PATH writes the exports to a 0o600
file instead of stdout (parent directory created 0o700 if needed) — useful for sourcing from a
shell profile instead of eval-ing directly. On Windows, those POSIX mode bits aren't enforced;
the file lands at its parent directory's inherited ACL instead. --profile selects a named
connection profile, but (like -grants) only its OIDC fields are honored — an
api_key_file-only profile yields no auth here.
--format {posix,powershell,cmd,dotenv} (default posix) picks the rendered syntax and applies
to both the stdout and --env-file output paths — it is never inferred from the OS (a Git Bash
prompt on Windows wants posix; pwsh runs on Linux/macOS too), so pass it explicitly:
--format |
rendered shape | consume with |
|---|---|---|
posix (default) |
export NAME=value, shlex.quoted |
eval "$(micromegas-setup-telemetry ...)" |
powershell |
$env:NAME = 'value' |
micromegas-setup-telemetry ... \| Invoke-Expression |
cmd |
@set "NAME=value" |
redirect to a .cmd file, then call it |
dotenv |
NAME=value, unquoted |
a compose service's env_file: or docker run --env-file to inject into the container; python-dotenv to inject into a process |
Each dialect quotes a value the way that syntax represents a literal credential safely: posix
via shlex.quote (bare unless the value needs quoting), powershell as a single-quoted literal
(a literal ' doubled), cmd as a quoted @set "NAME=value" (the only form that keeps quote
characters out of the value), and dotenv unquoted (the value is everything after the first
=). Because cmd and dotenv cannot represent every character, --otlp-endpoint is validated
against the chosen format before the key is minted, and the command errors out naming the
offending character rather than minting a key it then can't render (dotenv also rejects a
leading/trailing-whitespace endpoint, which a loader would silently trim). Passing --env-file
does not change --format's own default to dotenv — the documented use of --env-file is
sourcing it from a shell profile, and dotenv output is not a shell script, so posix stays the
default for both output paths.
Auth follows the same OIDC setup as micromegas-query/-screens/-grants.
Pass --version to print the installed package and interpreter version and exit.
Time Utilities¶
format_datetime(value), parse_datetime(value), and parse_time_delta(user_string)¶
Utility functions for time handling:
from micromegas.time import format_datetime, parse_datetime, parse_time_delta
# Format datetime for queries
dt = datetime.datetime.now(datetime.timezone.utc)
formatted = format_datetime(dt)
print(formatted) # "2024-01-01T12:00:00+00:00"
# Parse an RFC 3339 timestamp string (accepts both 'Z' and 'z')
parsed = parse_datetime('2024-01-01T12:00:00Z')
print(parsed) # 2024-01-01 12:00:00+00:00
# Parse human-readable time deltas
one_hour = parse_time_delta('1h')
thirty_minutes = parse_time_delta('30m')
seven_days = parse_time_delta('7d')
# Use in calculations
recent_time = datetime.datetime.now(datetime.timezone.utc) - parse_time_delta('2h')
Supported units: m (minutes), h (hours), d (days)
Advanced Features¶
Query Streaming Benefits¶
Use query_stream() for large datasets: it processes data in chunks instead of loading
everything into memory, starts returning results before the query completes, and can handle
result sets larger than available RAM.
# Example: Process week of data in batches
total_errors = 0
total_rows = 0
for batch in client.query_stream("""
SELECT level, msg FROM log_entries
WHERE time >= NOW() - INTERVAL '7 days'
""", begin, end):
df = batch.to_pandas()
errors_in_batch = len(df[df['level'] <= 2])
total_errors += errors_in_batch
total_rows += len(df)
print(f"Batch: {len(df)} rows, {errors_in_batch} errors")
print(f"Total: {total_rows} rows, {total_errors} errors")
FlightSQL Protocol Benefits¶
Micromegas uses Apache Arrow FlightSQL: columnar, binary data transfer with no serialization/deserialization overhead, native compression, and zero-copy reads from network buffers.
Error Handling¶
try:
df = client.query("SELECT * FROM log_entries;", begin, end)
except Exception as e:
print(f"Query failed: {e}")
# Check for empty results
if df.empty:
print("No data found for this time range")
else:
print(f"Found {len(df)} rows")
Exception types¶
The server classifies each query failure and returns a distinct gRPC status code, which pyarrow's Flight client surfaces as a different Python exception type:
| Cause | gRPC code | Exception raised |
|---|---|---|
| Bad query (typo'd function/column, syntax error, ...) | InvalidArgument |
pyarrow.lib.ArrowInvalid (a ValueError subclass) |
| Unimplemented feature | Unimplemented |
pyarrow.lib.ArrowNotImplementedError (a NotImplementedError subclass) |
| Query exceeded a resource budget (e.g. memory) | ResourceExhausted |
pyarrow.lib.ArrowInvalid (a ValueError subclass, message prefixed gRPC returned resource exhausted error) |
| Query rejected by an admin-managed query deny list rule | ResourceExhausted |
Same as the row above: pyarrow.lib.ArrowInvalid, same gRPC returned resource exhausted error message prefix -- the message itself additionally names the rule id and reason and tells you the remove_query_denial(...) call that lifts it |
CREATE MATERIALIZED VIEW of a name that already exists (without OR REPLACE) |
AlreadyExists |
pyarrow.lib.ArrowException |
DROP MATERIALIZED VIEW (without IF EXISTS) of a name that doesn't exist |
NotFound |
pyarrow.lib.ArrowKeyError (an ArrowException/KeyError subclass) |
| Genuine server-side bug | Internal |
pyarrow._flight.FlightInternalError |
This lets you distinguish "fix my query" from "something broke server-side" without parsing the
error message -- except that a resource-budget failure and a deny-list rejection share both the
same gRPC code and the same message prefix, so telling those two apart does need one more
signal: error_class in the query audit log is "resource" for the
former and "denied" for the latter.
try:
df = client.query(sql, begin, end)
except (ValueError, NotImplementedError) as e:
# ArrowInvalid (a ValueError) or ArrowNotImplementedError (a NotImplementedError):
# the query itself needs fixing -- except that a resource-budget failure and a
# query-deny-list rejection both also raise ArrowInvalid with the same
# "gRPC returned resource exhausted error" message prefix. Tell all three apart
# by error_class in the server-side audit log: "user" / "resource" / "denied".
print(f"Bad query: {e}")
except Exception as e:
# FlightInternalError / anything else: a genuine server-side problem, not
# something to fix in the SQL text.
print(f"Query failed: {e}")
Performance Tips¶
Use Time Ranges¶
Always specify time ranges for better performance:
# ✅ Good - efficient
df = client.query(sql, begin, end)
# ❌ Avoid - can be slow
df = client.query(sql)
Streaming for Large Results¶
Use streaming for queries that might return large datasets:
# If you expect > 100MB of results, use streaming
if expected_result_size_mb > 100:
for batch in client.query_stream(sql, begin, end):
process_batch(batch.to_pandas())
else:
df = client.query(sql, begin, end)
process_dataframe(df)
Limit Result Size¶
Add LIMIT clauses for exploratory queries:
# Good for exploration
df = client.query("SELECT * FROM log_entries LIMIT 1000;", begin, end)
# Then remove limit for production queries
df = client.query("SELECT * FROM log_entries WHERE level <= 2;", begin, end)
Integration Examples¶
Jupyter Notebooks¶
import matplotlib.pyplot as plt
import seaborn as sns
# Query data
df = client.query("""
SELECT time, name, value
FROM measures
WHERE name = 'cpu_usage'
""", begin, end)
# Plot time series
plt.figure(figsize=(12, 6))
plt.plot(df['time'], df['value'])
plt.title('CPU Usage Over Time')
plt.xlabel('Time')
plt.ylabel('CPU Usage %')
plt.show()
Data Pipeline¶
import pandas as pd
def extract_metrics(process_id, hours=24):
"""Extract metrics for a specific process."""
end = datetime.datetime.now(datetime.timezone.utc)
begin = end - datetime.timedelta(hours=hours)
sql = f"""
SELECT time, name, value, unit
FROM view_instance('measures', '{process_id}')
ORDER BY time;
"""
return client.query(sql, begin, end)
def analyze_performance(df):
"""Analyze performance metrics."""
metrics = {}
for name in df['name'].unique():
data = df[df['name'] == name]['value']
metrics[name] = {
'mean': data.mean(),
'max': data.max(),
'min': data.min(),
'std': data.std()
}
return metrics
# Use in pipeline
process_metrics = extract_metrics('my-service-123')
performance_summary = analyze_performance(process_metrics)
print(performance_summary)
Next Steps¶
- Python API Advanced - Advanced patterns, performance optimization, and specialized tooling
- Schema Reference - Understand available views and fields
- Functions Reference - Learn about SQL functions
- Query Patterns - Common observability query patterns
- Performance Guide - Optimize your queries