Architecture¶
1. Default Topology / Flow¶
flowchart TB
subgraph Sources["Data Sources"]
OTEL_S["OTel Collector\n(OTLP gRPC/HTTP)"]
PROM_S["Prometheus\n(remote_write)"]
ES_S["ES Bulk API\nclients"]
FB_S["FluentBit /\nVector"]
KF_S["Kinesis Firehose\n/ GCP Pub/Sub"]
RUM_S["RUM SDK\n(browser)"]
end
subgraph O2Cluster["OpenObserve Cluster"]
direction TB
subgraph Stateless["Stateless Compute"]
Router["Router\n(request dispatch)"]
Ingester["Ingester\n(WAL → Parquet)"]
Querier["Querier\n(DataFusion engine)"]
Compactor["Compactor\n(file merging)"]
AlertMgr["AlertManager\n(alerts + reports)"]
end
subgraph Infra["Infrastructure"]
WAL["WAL\n(local disk\nmemtable)"]
Cache["Disk Cache\n(querier-side)"]
end
end
subgraph Storage["Storage Layer"]
S3["Object Storage\n(S3/GCS/Azure/MinIO)"]
PQ["Apache Parquet\n(Zstd compressed)"]
Meta["Metadata Store\n(PostgreSQL / SQLite)"]
end
Sources --> Router
Router --> Ingester
Ingester --> WAL
WAL -->|"flush every\n5min or size"| PQ
PQ --> S3
Querier -->|"scan"| S3
Querier --> Cache
Compactor -->|"merge"| S3
AlertMgr --> Querier
style Stateless fill:#e65100,color:#fff
style Storage fill:#1565c0,color:#fff
Component breakdown, deployment topologies, and storage architecture for OpenObserve.
System Architecture¶
Node Role Architecture¶
flowchart LR
subgraph Roles["ZO_NODE_ROLE"]
ALL["all\n(single node)"]
R["router"]
I["ingester"]
Q["querier"]
C["compactor"]
A["alertmanager"]
end
subgraph Groups["ZO_NODE_ROLE_GROUP"]
Default["default\n(user queries)"]
Background["background\n(alerts, reports)"]
end
R --> I
R --> Q
R --> A
Q -.- Default
A -.- Background
Role Responsibility¶
| Role | State | Scales | CPU Profile | Memory Profile |
|---|---|---|---|---|
| Router | Stateless | Horizontal | Low | Low |
| Ingester | WAL on disk | Horizontal | Medium | Medium (memtable) |
| Querier | Cache on disk | Horizontal | High (DataFusion) | High (scan buffers) |
| Compactor | Stateless | 1–2 nodes | Medium | Low |
| AlertManager | Stateless | 1–2 nodes | Low | Low |
Storage Architecture¶
Data Path¶
sequenceDiagram
participant Client as Client
participant Ingester as Ingester
participant WAL as Local WAL
participant S3 as Object Storage
participant Compactor as Compactor
Client->>Ingester: JSON / OTLP / ES Bulk
Ingester->>Ingester: Schema inference
Ingester->>WAL: Write to memtable (Arrow batches)
Note over WAL: Flush triggers:<br/>5 min elapsed OR<br/>file size threshold
WAL->>S3: Write small Parquet file
Note over S3: Small files (1-10 MB)
loop Background compaction
Compactor->>S3: Read small files
Compactor->>Compactor: Sort, merge, re-partition
Compactor->>S3: Write large Parquet file
Compactor->>S3: Delete old small files
end
Note over S3: Large files (100+ MB)<br/>Sorted by time, partitioned by stream
Parquet File Structure¶
| Layer | Detail |
|---|---|
| Partitioning | By organization → stream → date → time window |
| Compression | Zstd (default), high compression ratio |
| Bloom filters | Per-column, configurable for high-cardinality fields |
| Row groups | Optimized for DataFusion predicate pushdown |
| Metadata | Column statistics for partition pruning |
Query Engine: DataFusion¶
flowchart LR
SQL["SQL Query"] --> Parser["SQL Parser"]
Parser --> LP["Logical Plan"]
LP --> Opt["Optimizer\n(predicate pushdown,\nprojection pruning,\npartition pruning)"]
Opt --> PP["Physical Plan"]
PP --> Scan["Parquet Scanner\n(parallel, columnar)"]
Scan --> S3_Q["Read from S3\n(only needed cols)"]
S3_Q --> Exec["Vectorized Execution\n(Arrow batches)"]
Exec --> Result["Query Result"]
style Opt fill:#2e7d32,color:#fff
HA Deployment Topology¶
flowchart TB
LB["Load Balancer"]
subgraph Routers["Router Pool"]
R1["Router 1"]
R2["Router 2"]
end
subgraph Ingesters["Ingester Pool"]
I1["Ingester 1\n(WAL /data1)"]
I2["Ingester 2\n(WAL /data2)"]
I3["Ingester 3\n(WAL /data3)"]
end
subgraph Queriers["Querier Pool"]
Q1["Querier 1\n(cache /cache1)"]
Q2["Querier 2\n(cache /cache2)"]
end
C1["Compactor"]
A1["AlertManager"]
S3_HA["S3 / MinIO\n(shared storage)"]
PG["PostgreSQL\n(metadata)"]
LB --> Routers
R1 --> Ingesters
R2 --> Ingesters
R1 --> Queriers
R2 --> Queriers
Ingesters --> S3_HA
Queriers --> S3_HA
C1 --> S3_HA
A1 --> Queriers
Routers --> PG
Ingesters --> PG
Queriers --> PG
style S3_HA fill:#1565c0,color:#fff
Sources¶
Data Model¶
1. Default Topology / Flow¶
erDiagram
Openobserve_CORE ||--o{ CONFIG : requires
Openobserve_CORE ||--o{ STATE : writes
CONFIG {
string runtime_params
string limits
}
STATE {
string metric_id
json payload
}
How It Works¶
How OpenObserve uses Rust, Apache Parquet, and object storage to deliver cost-efficient observability.
Architecture¶
Component Roles¶
flowchart TB
subgraph Sources["Data Sources"]
OTEL["OTel Collector"]
PROM["Prometheus"]
FB["FluentBit / Vector"]
ES_API["ES Bulk API\nclients"]
RUM_SDK["RUM SDK"]
KF["Kinesis Firehose"]
end
subgraph O2["OpenObserve Cluster"]
direction TB
Router["Router\n(request dispatch)"]
Ingester["Ingester\n(WAL → Parquet)"]
Querier["Querier\n(DataFusion)"]
Compactor["Compactor\n(file merging)"]
Alert["AlertManager\n(alerts + reports)"]
end
subgraph Storage["Storage Layer"]
WAL["WAL\n(local disk)"]
S3["Object Storage\n(S3/GCS/Azure/MinIO)"]
Meta["Metadata Store\n(PostgreSQL / SQLite)"]
end
Sources --> Router
Router --> Ingester
Ingester -->|batch| WAL
WAL -->|flush| S3
Querier --> S3
Compactor --> S3
Alert --> Querier
style O2 fill:#e65100,color:#fff
Data Flow: Ingestion to Query¶
sequenceDiagram
participant Client as Data Source
participant Router as Router
participant Ingester as Ingester
participant WAL as Local WAL
participant S3 as Object Storage
participant Compactor as Compactor
participant Querier as Querier
Client->>Router: OTLP / ES Bulk / Prom RW
Router->>Ingester: Route by stream
Ingester->>Ingester: Schema inference
Ingester->>WAL: Write to memtable
WAL->>S3: Flush as Parquet (5min or size threshold)
Note over S3: Small Parquet files
Compactor->>S3: Merge small files
Compactor->>S3: Write optimized Parquet
Note over S3: Large, sorted Parquet files
Querier->>S3: Read Parquet partitions
Querier->>Querier: DataFusion vectorized execution
Querier->>Client: Return results
Storage Architecture¶
Parquet on Object Storage¶
OpenObserve stores all data as Apache Parquet files on object storage:
| Aspect | Detail |
|---|---|
| Format | Apache Parquet (columnar) |
| Compression | Zstd (default) |
| Partitioning | By stream, date, and time window |
| Object storage | S3, GCS, Azure Blob, MinIO |
| Local mode | Disk-backed (for dev/single-node) |
| Bloom filters | Per-column for high-cardinality field acceleration |
Why 140x Cheaper Than Elasticsearch¶
| Factor | OpenObserve | Elasticsearch |
|---|---|---|
| Compression | Parquet columnar + Zstd (~10:1) | Lucene segments (~1.5:1) |
| Storage tier | S3 ($0.023/GB/mo) | SSD ($0.10+/GB/mo) |
| Compute | Stateless, scale to zero | Always-on data nodes |
| Replicas | S3 provides 11-nines | Manually replicated shards |
| Net effect | ~$0.002/GB/mo storage | ~$0.28/GB/mo storage |
Query Engine: Apache Arrow DataFusion¶
OpenObserve uses DataFusion (Apache Arrow's query engine) for SQL execution:
- SQL parsing: the parser turns the user query into a logical plan
- Optimization: Predicate pushdown, projection pruning, partition pruning
- Parquet scanning: the scanner reads only the needed columns and row groups from S3
- Vectorized execution: the engine processes Arrow columnar batches in CPU cache-friendly patterns
- Aggregation: Final merge across partitions
Query Language Support¶
| Signal | Language | Example |
|---|---|---|
| Logs | SQL | SELECT * FROM logs WHERE body LIKE '%error%' ORDER BY _timestamp DESC LIMIT 100 |
| Traces | SQL | SELECT * FROM traces WHERE service_name='api' AND duration > 1000 |
| Metrics | PromQL | rate(http_requests_total[5m]) |
Node Roles¶
In HA mode, OpenObserve separates concerns via ZO_NODE_ROLE:
| Role | Responsibility |
|---|---|
router |
Dispatches requests to correct backend node |
ingester |
Receives data, writes WAL, flushes Parquet to S3 |
querier |
Reads Parquet from S3, executes queries |
compactor |
Merges small Parquet files for efficiency |
alertmanager |
Evaluates alert rules, generates notifications |
Workload Separation¶
ZO_NODE_ROLE_GROUP prevents resource-intensive queries (alerts, reports) from impacting real-time user searches by routing them to dedicated node groups.
Pipelines¶
OpenObserve supports ingestion-time pipelines for data transformation:
- Field extraction: Parse structured data from log lines
- Enrichment: Add fields from lookup tables
- Filtering: Drop unwanted logs before storage
- Routing: Send data to different streams based on content
Pipelines use VRL (Vector Remap Language) for transformation logic.
Sources¶
Benchmarks¶
Storage efficiency, query performance, and cost analysis for OpenObserve.
Storage Efficiency vs Elasticsearch¶
Architectural Comparison¶
| Factor | OpenObserve | Elasticsearch |
|---|---|---|
| Storage format | Parquet (columnar) + Zstd | Lucene segments (row-oriented) + inverted index |
| Compression ratio | ~10:1 | ~1.5:1 |
| Storage tier | Object storage (S3: $0.023/GB/mo) | SSD ($0.10+/GB/mo) |
| Indexing | Optional per-column bloom filters | Full inverted index on every field |
| Net storage cost | ~$0.002/GB/mo | ~$0.28/GB/mo |
| Claimed advantage | ~140x cheaper storage | — |
Why 140x Cheaper¶
The 140x claim combines two factors:
- Compression: Parquet columnar + Zstd achieves ~10:1 vs Lucene's ~1.5:1 → ~7x less raw bytes
- Storage tier: S3 ($0.023/GB/mo) vs SSD ($0.10+/GB/mo) → ~4–5x cheaper per GB
- No replica overhead: S3 provides 11-nines durability natively vs manually replicated Elasticsearch shards → ~2–3x savings
Combined: ~7x × ~4x × ~2.5x ≈ ~70–140x depending on configuration.
Caveat: This is a vendor-provided comparison. Actual ratios depend on data patterns, compression ratios, and S3 pricing tiers.
Query Performance¶
Analytical Queries (Aggregations)¶
| Aspect | OpenObserve | Elasticsearch |
|---|---|---|
| Column pruning | Yes (read only needed columns) | No (reads full documents) |
| Predicate pushdown | Yes (DataFusion → Parquet row group stats) | Partial (inverted index) |
| Vectorized execution | Yes (Apache Arrow batches) | No |
| Aggregation speed | Often faster for analytical patterns | Faster for full-text search |
Full-Text Search¶
| Aspect | OpenObserve | Elasticsearch |
|---|---|---|
| Approach | Parquet scan + bloom filters | Inverted index |
| Wildcard search | Full scan (slower) | Fast (inverted index) |
| Best for | Known-field searches, aggregations | Complex full-text search |
Resource Footprint¶
Single-Node (Dev/POC)¶
| Metric | Value |
|---|---|
| Binary size | ~50 MB |
| Startup time | < 5 seconds |
| Idle RAM | ~50–100 MB |
| Minimum resources | 1 CPU, 512 MB RAM |
Production HA¶
| Component | CPU | RAM | Storage |
|---|---|---|---|
| Ingester (×3) | 2 vCPU | 4 GB | 100 GB WAL disk |
| Querier (×2) | 4 vCPU | 8 GB | 50 GB cache disk |
| Compactor (×1) | 2 vCPU | 4 GB | 50 GB temp |
| Router (×2) | 1 vCPU | 1 GB | — |
| AlertManager (×1) | 1 vCPU | 1 GB | — |
| PostgreSQL | 1 vCPU | 2 GB | 20 GB |
| S3 | — | — | Unlimited |
Cost Comparison (100 GB/day logs, 30-day retention)¶
| Cost Item | OpenObserve (self-hosted) | Elasticsearch (self-hosted) |
|---|---|---|
| Storage | ~$7/mo (S3, 300 GB after compression) | ~$300/mo (3 TB SSD, 3× replicated) |
| Compute | ~$500/mo (small stateless nodes) | ~$1,500/mo (3× data nodes, 64 GB each) |
| Total | ~$507/mo | ~$1,800/mo |
Scale Limits¶
| Dimension | Practical Limit | Notes |
|---|---|---|
| Daily ingestion | PB-scale | S3 write throughput bottleneck |
| Query concurrency | 50–100 | Add querier replicas |
| Retention | Unlimited | S3 lifecycle policies |
| Streams (indices) | 10,000+ | Metadata store can need PostgreSQL |
| Single query scan | TB-range | DataFusion parallelizes across partitions |
Caveats¶
- 140x cost claim is from vendor benchmarks and combines compression + storage tier + replication savings.
- Full-text search performance lags behind Elasticsearch's inverted index for wildcard/fuzzy queries.
- DataFusion is less battle-tested than ClickHouse or Elasticsearch at extreme scale.
- Performance varies significantly with data patterns and query types.
Sources¶
Security¶
Security model for OpenObserve covering multi-tenancy, RBAC, API token management, S3 bucket encryption, and authentication. See also: observability/openobserve/index, observability/openobserve/explanation, observability/openobserve/how-to-guides.
Security Architecture Overview¶
OpenObserve provides three tiers of security capability depending on edition:
| Feature | Open Source | Enterprise | Cloud |
|---|---|---|---|
| Basic auth (username/password) | Yes | Yes | Yes |
| Multiple users | Yes | Yes | Yes |
| Multi-tenancy (organizations) | Yes | Yes | Yes |
| RBAC (predefined roles) | No | Yes (with OpenFGA) | Yes (preconfigured) |
| Custom roles | No | Yes | Yes |
| User groups | No | Yes | Yes |
| Service accounts | No | Yes | Yes |
| SSO (SAML/OIDC) | No | Yes | Yes |
flowchart TD
subgraph "External"
Clients[API Clients<br/>Collectors / Agents]
Users[Browser Users]
IdP[Identity Provider<br/>SAML / OIDC / Dex]
end
subgraph "OpenObserve"
API[API Gateway<br/>Basic Auth Validation]
RBAC[RBAC Engine<br/>OpenFGA Enterprise]
Ingester[Ingester<br/>Data Processing]
Querier[Querier<br/>SQL / PromQL]
end
subgraph "Storage"
Meta[(Meta Store<br/>PostgreSQL / SQLite)]
S3[S3 / GCS / Azure Blob<br/>SSE-KMS Encrypted]
WAL[Local WAL<br/>Disk Buffer]
end
Clients -->|Authorization: Basic base64 user:pass| API
Users -->|Login / SSO| API
API -->|Validate credentials| Meta
API -->|Check permissions| RBAC
RBAC --> IdP
API --> Ingester
API --> Querier
Ingester --> WAL --> S3
Querier --> S3
Authentication¶
Basic Authentication¶
All OpenObserve API endpoints require HTTP Basic authentication:
The root user is configured at startup:
auth:
ZO_ROOT_USER_EMAIL: "[email protected]"
ZO_ROOT_USER_PASSWORD: "secure-password"
ZO_ROOT_USER_TOKEN: ""
Root User
The root user has unrestricted access to all organizations and settings. The root token (ZO_ROOT_USER_TOKEN) can be used for API automation.
SSO Integration (Enterprise)¶
OpenObserve Enterprise supports SAML 2.0 and OIDC for single sign-on. The Dex identity service is commonly deployed alongside OpenObserve for OIDC:
# values.yaml for Enterprise Edition
openfga:
enabled: true # Required for RBAC
dex:
enabled: true # OIDC identity provider
SSO configuration steps: 1. Deploy OpenFGA for authorization decisions. 2. Deploy Dex (or configure an external IdP) for authentication. 3. Map IdP groups to OpenObserve roles via the UI or API.
Multi-Tenancy¶
OpenObserve uses organizations as the primary multi-tenancy boundary. Each organization has isolated:
- Streams (logs, metrics, traces)
- Dashboards
- Alerts
- Pipelines
- User permissions
Organization Isolation¶
# Query data in a specific organization
GET /api/{organization}/_search
Authorization: Basic base64("user:pass")
- Data from Organization A is not accessible from Organization B unless the user has explicit permissions in both.
- Each organization can have separate retention policies, stream settings, and pipeline configurations.
- Quotas can be set per organization (Enterprise) to limit ingestion volume, storage, and query frequency.
Stream-Level Quotas (Enterprise)¶
Organizations can enforce quotas to prevent noisy-neighbor scenarios:
- Maximum ingestion rate per stream.
- Maximum storage per organization.
- Maximum query frequency per user.
Role-Based Access Control¶
Predefined Roles (Enterprise/Cloud)¶
OpenObserve provides five built-in roles:
| Role | Dashboards | Streams | Alerts | Users | Settings |
|---|---|---|---|---|---|
| Root | All | All | All | All | All |
| Admin | All (org) | All (org) | All (org) | Manage (org) | All (org) |
| Editor | Create/Edit | Read/Write | Create/Edit | None | None |
| Viewer | View only | Read only | View only | None | None |
| User | Limited | Limited | None | None | None |
Custom Roles (Enterprise)¶
Administrators can create custom roles with fine-grained permissions using OpenFGA:
POST /api/roles
{
"name": "logs-analyst",
"permissions": [
"streams:read",
"streams:query",
"dashboards:read",
"dashboards:create"
]
}
User Groups (Enterprise)¶
Users can be organized into groups, and permissions assigned at the group level:
- Group membership managed via the UI or API.
- Role assignments inherited by all group members.
- Simplifies permission management for large teams.
Service Accounts (Enterprise)¶
Service accounts provide machine-to-machine authentication with role-based token access:
- Tokens are scoped to the service account's role and organization.
- Separate from user accounts. Not affected by user lifecycle events.
- Suitable for collectors, CI/CD pipelines, and automation tools.
API Token Management¶
Root Token¶
The root token is configured at deployment time:
This token grants unrestricted API access. Protect it as a secret:
# Kubernetes secret
kubectl create secret generic openobserve-root-token \
--from-literal=token="$(openssl rand -hex 32)"
User Tokens¶
Each user authenticates via Basic Auth. The API validates credentials against the metadata store (PostgreSQL or SQLite). There is no separate token issuance for OSS users. Enterprise users can create service account tokens.
Object Storage Encryption¶
OpenObserve stores all telemetry data in S3-compatible object storage as compressed Parquet files. Encryption at rest is enforced at the bucket level.
S3 Configuration¶
auth:
ZO_S3_ACCESS_KEY: "AKIAIOSFODNN7EXAMPLE"
ZO_S3_SECRET_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
config:
ZO_S3_BUCKET_NAME: "openobserve-prod"
ZO_S3_REGION_NAME: "us-west-1"
EKS IAM Role Integration¶
On Amazon EKS, use IAM Roles for Service Accounts (IRSA) instead of static credentials:
This eliminates static S3 credentials from the deployment and relies on temporary, automatically-rotated IAM credentials.
Encryption Recommendations¶
| Method | Use Case |
|---|---|
| SSE-S3 | Default. AWS-managed keys |
| SSE-KMS | Recommended. Audit trail via CloudTrail. Customer-managed key rotation |
| SSE-C | Maximum control. The client provides the key per request |
Bucket policies must restrict access to the OpenObserve service account only:
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::openobserve-prod/*",
"Condition": {
"StringNotEquals": {"aws:PrincipalArn": "arn:aws:iam::123456789:role/zo-s3-eks"}
}
}
Metadata Store Security¶
OpenObserve uses PostgreSQL (recommended for production) or SQLite (development) for metadata:
auth:
ZO_META_POSTGRES_DSN: "postgres://openobserve:password@postgres:5432/openobserve?sslmode=require"
- Always use
sslmode=requirefor PostgreSQL connections in production. - Create a dedicated database user with schema-level permissions only.
- Store the DSN as a Kubernetes secret, not in plaintext values.
Network Security¶
| Port | Protocol | Purpose | Exposure |
|---|---|---|---|
| 5080 | HTTP | Web UI and API | Behind TLS proxy |
| 5081 | HTTP | gRPC ingestion (OTLP) | Internal only |
| 5082 | HTTP | Internal cluster communication | Internal only |
TLS Termination¶
OpenObserve does not include built-in TLS termination. Deploy behind a reverse proxy (Nginx, Envoy, AWS ALB) for HTTPS:
server {
listen 443 ssl;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
proxy_pass http://openobserve:5080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Hardening Checklist¶
| Area | Recommendation |
|---|---|
| Root credentials | Change default password immediately. Use a strong password or token |
| TLS | Terminate TLS at reverse proxy. Enforce HTTPS |
| S3 encryption | Use SSE-KMS with customer-managed keys |
| S3 credentials | Use IAM roles (IRSA) instead of static access keys |
| PostgreSQL | Enable sslmode=require. Use a dedicated user |
| RBAC | Enable OpenFGA for granular permissions (Enterprise) |
| Service accounts | Use dedicated tokens for collectors and automation |
| Network | Restrict gRPC ingestion port to internal traffic only |
| Secrets | Store all credentials in Kubernetes secrets or Vault |
| Quotas | Set ingestion and storage quotas per organization |