Data Model
IceGate stores observability data in five tenant-scoped Apache Iceberg tables - logs, spans, events, metrics, and operations - plus one global reference table, prices.
Table Overview
| Table | Description | Primary Use Case |
|---|---|---|
logs |
OpenTelemetry LogRecords | Application logging |
spans |
Distributed trace spans | Request tracing |
events |
Semantic events | Business events, alerts |
metrics |
All metric types | Performance monitoring |
operations |
LLM and agent operations | Token usage, cost, prompt and completion capture |
prices |
Global LLM rate card (no tenant_id) |
Reference rates for costing operations |
Common Design Patterns
Multi-Tenancy
The five tenant-scoped tables use identity partitioning on tenant_id. prices is reference data shared by every tenant, so it carries no tenant_id and is partitioned differently:
partitioning = ARRAY['tenant_id', 'account_id', 'day(timestamp)']
Attributes Storage
Attributes are stored as MAP(VARCHAR, VARCHAR) merging:
- Resource attributes
- Scope attributes
- Record-level attributes
Time Precision
All timestamps use microsecond precision with timezone:
TIMESTAMP(6) WITH TIME ZONE
Compression
All tables use ZSTD compression for optimal size/speed balance.
Logs Table
Based on OpenTelemetry LogRecord.
CREATE TABLE logs (
tenant_id VARCHAR NOT NULL,
account_id VARCHAR,
service_name VARCHAR,
timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
observed_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
ingested_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
trace_id VARBINARY, -- 16 bytes
span_id VARBINARY, -- 8 bytes
severity_number INTEGER,
severity_text VARCHAR,
body VARCHAR,
attributes MAP(VARCHAR, VARCHAR) NOT NULL,
flags INTEGER,
dropped_attributes_count INTEGER NOT NULL
)
Sorting: service_name, timestamp DESC (recent-first)
Severity Levels
| Number | Text | Description |
|---|---|---|
| 1-4 | TRACE | Detailed debugging |
| 5-8 | DEBUG | Debug information |
| 9-12 | INFO | Normal operations |
| 13-16 | WARN | Warning conditions |
| 17-20 | ERROR | Error conditions |
| 21-24 | FATAL | Critical failures |
Spans Table
Based on OpenTelemetry Span with nested events and links.
CREATE TABLE spans (
tenant_id VARCHAR NOT NULL,
account_id VARCHAR,
trace_id VARBINARY NOT NULL, -- 16 bytes
span_id VARBINARY NOT NULL, -- 8 bytes
parent_span_id VARBINARY, -- 8 bytes
timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
end_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
ingested_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
duration_micros BIGINT NOT NULL,
trace_state VARCHAR,
name VARCHAR NOT NULL,
kind INTEGER,
status_code INTEGER,
status_message VARCHAR,
attributes MAP(VARCHAR, VARCHAR) NOT NULL,
flags INTEGER,
dropped_attributes_count INTEGER,
dropped_events_count INTEGER,
dropped_links_count INTEGER,
events ARRAY(ROW(
timestamp TIMESTAMP(6) WITH TIME ZONE,
name VARCHAR,
attributes MAP(VARCHAR, VARCHAR),
dropped_attributes_count INTEGER
)),
links ARRAY(ROW(
trace_id VARBINARY,
span_id VARBINARY,
trace_state VARCHAR,
attributes MAP(VARCHAR, VARCHAR),
dropped_attributes_count INTEGER,
flags INTEGER
))
)
Sorting: trace_id, timestamp (group spans by trace)
Span Kind
| Value | Name | Description |
|---|---|---|
| 0 | UNSPECIFIED | Not specified |
| 1 | INTERNAL | Internal operation |
| 2 | SERVER | Server-side request |
| 3 | CLIENT | Client-side request |
| 4 | PRODUCER | Message producer |
| 5 | CONSUMER | Message consumer |
Status Code
| Value | Name | Description |
|---|---|---|
| 0 | UNSET | Status not set |
| 1 | OK | Operation successful |
| 2 | ERROR | Operation failed |
Events Table
Semantic events extracted from logs.
CREATE TABLE events (
tenant_id VARCHAR NOT NULL,
account_id VARCHAR,
service_name VARCHAR,
timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
observed_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
ingested_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
event_domain VARCHAR NOT NULL,
event_name VARCHAR NOT NULL,
trace_id VARBINARY,
span_id VARBINARY,
attributes MAP(VARCHAR, VARCHAR) NOT NULL
)
Sorting: service_name, timestamp DESC
Events follow OpenTelemetry semantic conventions:
event_domain: Category (e.g., "user", "system")event_name: Specific event (e.g., "login", "error")
Metrics Table
All OpenTelemetry metric types in a unified table.
CREATE TABLE metrics (
tenant_id VARCHAR NOT NULL,
account_id VARCHAR,
service_name VARCHAR NOT NULL,
timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
start_timestamp TIMESTAMP(6) WITH TIME ZONE,
ingested_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
metric_name VARCHAR NOT NULL,
metric_type VARCHAR NOT NULL,
description VARCHAR,
unit VARCHAR,
aggregation_temporality VARCHAR,
is_monotonic BOOLEAN,
attributes MAP(VARCHAR, VARCHAR) NOT NULL,
-- Gauge/Sum values
value_double DOUBLE,
value_int BIGINT,
-- Histogram values
count BIGINT,
sum DOUBLE,
min DOUBLE,
max DOUBLE,
bucket_counts ARRAY(BIGINT),
explicit_bounds ARRAY(DOUBLE),
-- Exponential histogram
scale INTEGER,
zero_count BIGINT,
zero_threshold DOUBLE,
positive_offset INTEGER,
positive_bucket_counts ARRAY(BIGINT),
negative_offset INTEGER,
negative_bucket_counts ARRAY(BIGINT),
-- Summary
quantile_values ARRAY(ROW(
quantile DOUBLE,
value DOUBLE
)),
-- Exemplars
flags INTEGER,
exemplars ARRAY(ROW(
timestamp TIMESTAMP(6) WITH TIME ZONE,
value_double DOUBLE,
value_int BIGINT,
span_id VARBINARY,
trace_id VARBINARY,
attributes MAP(VARCHAR, VARCHAR)
))
)
Sorting: metric_name, service_name, timestamp DESC
Metric Types
| Type | Fields Used |
|---|---|
gauge |
value_double or value_int |
sum |
value_double or value_int, is_monotonic, aggregation_temporality |
histogram |
count, sum, min, max, bucket_counts, explicit_bounds |
exponential_histogram |
count, sum, scale, zero_count, positive_*, negative_* |
summary |
count, sum, quantile_values |
Operations Table
LLM and agent operations, following the OpenTelemetry generative-AI semantic conventions.
CREATE TABLE operations (
tenant_id VARCHAR NOT NULL,
conversation_id VARCHAR,
-- identity
trace_id VARBINARY NOT NULL,
span_id VARBINARY NOT NULL,
parent_span_id VARBINARY,
service_name VARCHAR,
scope_name VARCHAR,
scope_version VARCHAR,
-- timing
timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
end_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
duration_micros BIGINT NOT NULL,
ingested_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
operation_name VARCHAR NOT NULL,
-- provider and model
provider_name VARCHAR,
request_model VARCHAR,
response_model VARCHAR,
response_id VARCHAR,
-- sampling parameters
temperature DOUBLE,
top_p DOUBLE,
top_k BIGINT,
max_tokens BIGINT,
frequency_penalty DOUBLE,
presence_penalty DOUBLE,
seed BIGINT,
stream BOOLEAN,
choice_count BIGINT,
output_type VARCHAR,
reasoning_effort VARCHAR,
time_to_first_chunk_ms BIGINT,
-- token usage
input_tokens BIGINT,
output_tokens BIGINT,
total_tokens BIGINT,
reasoning_tokens BIGINT,
cache_creation_input_tokens BIGINT,
cache_read_input_tokens BIGINT,
user_id VARCHAR,
-- tool calls
tool_name VARCHAR,
tool_call_id VARCHAR,
tool_type VARCHAR,
tool_description VARCHAR,
data_source_id VARCHAR,
embedding_dimensions INTEGER,
-- server and status
server_address VARCHAR,
server_port INTEGER,
status_code INTEGER,
status_message VARCHAR,
error_type VARCHAR,
-- agent and workflow
agent_id VARCHAR,
agent_name VARCHAR,
agent_version VARCHAR,
agent_description VARCHAR,
workflow_name VARCHAR,
-- content, JSON-encoded
input_messages VARCHAR,
output_messages VARCHAR,
system_instructions VARCHAR,
tool_definitions VARCHAR,
tool_call_arguments VARCHAR,
tool_call_result VARCHAR,
stop_sequences ARRAY(VARCHAR),
finish_reasons ARRAY(VARCHAR),
encoding_formats ARRAY(VARCHAR)
)
Partitioning: tenant_id (identity), day(timestamp)
Sorting: trace_id, timestamp DESC - clusters a trace's operations together, recent first
The six VARCHAR content columns (input_messages, output_messages, system_instructions, tool_definitions, tool_call_arguments, tool_call_result) hold JSON-encoded payloads rather than parsed structures, so prompt and completion shapes can vary per provider without a schema change.
Prices Table
A global LLM rate card, populated by the Maintain service's pricing crawler from the OpenRouter and LiteLLM feeds.
Unlike the five telemetry tables it carries no tenant_id - rates are reference data, identical for every tenant. It is an append-only observation log: a row is written only when a rate first differs from the previous one for its key, and valid_to is derived at query time.
Key: (provider, model, service_tier, region, min_input_tokens, valid_from)
Context tiers and service tiers live in the key rather than in extra columns, so the rate columns stay flat as the card grows. Rate columns are DECIMAL(38, 10) rather than floating point - money has to be exact, and binary f64 cannot represent a value like 0.075 or sum it without drift.
Joining Prices to Operations
The query engine exposes a derived view, prices_effective, which adds valid_to - the next revision's valid_from for the same key, NULL for the row currently in effect. It is a DataFusion object, so the Loki, Tempo, and Flight SQL paths see it; Trino reads the Iceberg catalog directly and does not, which is why the raw table stays self-sufficient.
Warning
IceGate does not compute cost, and operations does not carry the full pricing key. It records provider_name and request_model, which line up with prices.provider and prices.model, but nothing for service_tier, region, or min_input_tokens. A cost query has to supply those three from deployment knowledge - a fixed tier and region per account, say. Treat such a join as an estimate parameterised by your own assumptions, not a derivation the schema guarantees.
Query Examples
Logs Query
SELECT timestamp, severity_text, body
FROM logs
WHERE tenant_id = 'my-tenant'
AND service_name = 'api-service'
AND timestamp >= TIMESTAMP '2025-01-01 00:00:00 UTC'
ORDER BY timestamp DESC
LIMIT 100;
Trace Reconstruction
SELECT span_id, name, duration_micros
FROM spans
WHERE tenant_id = 'my-tenant'
AND trace_id = X'5B8EFFF798038103D269B633813FC60C'
ORDER BY timestamp;
Metrics Aggregation
SELECT
date_trunc('hour', timestamp) AS hour,
avg(value_double) AS avg_value
FROM metrics
WHERE tenant_id = 'my-tenant'
AND metric_name = 'http_request_duration_seconds'
GROUP BY 1
ORDER BY 1;
Next Steps
- Learn about Architecture
- Explore Querying
- See Deployment