Skip to content

alesr/khipu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

khipu

khipu (/ˈkiːpuː/) — an Inca method for recording information with knotted cords.

khipu is an OTLP metrics ingest service written in Go. It receives OTLP/gRPC metrics, extracts series metadata into a lookup table, and stores datapoints in ClickHouse with a SeriesID reference.

Supported metric types today:

  • Gauge
  • Sum

Quick Start

Prerequisites

  • Go 1.26+
  • Docker

1. Start ClickHouse

make db-up

2. Run khipu

make run

By default, khipu listens on localhost:4317 (OTLP gRPC).

3. Generate traffic

make metricgen

4. Open ClickHouse shell

make ch

5. Stop local infra

make db-down

Cookbook

For query examples and a complete walkthrough, see:


Build, Run, and Test Commands

Use:

make help

This prints all available targets and descriptions (build, run, db-up/down, metricgen, test, test-integration, test-all, and more).


Configuration

Configuration is loaded from .env / environment variables.

You can start from:

cp .env.example .env

Main variables:

Env var Default Description
LISTEN_ADDR localhost:4317 OTLP gRPC listen address
CLICKHOUSE_ADDR localhost:9000 ClickHouse native endpoint
CLICKHOUSE_DATABASE default ClickHouse database
CLICKHOUSE_USER default ClickHouse user
CLICKHOUSE_PASSWORD dev ClickHouse password
SERIES_CACHE_CAPACITY 100000 In-memory series cache size
MAX_RECV_MSG_SIZE 16777216 Max gRPC receive message size
DASH0_ENABLED false Export service telemetry via OTLP
OTEL_EXPORTER_OTLP_ENDPOINT (empty) OTLP endpoint when Dash0/remote export is enabled

Metric generator variables:

Env var Default Description
GEN_INTERVAL 1s Export interval
GEN_SERVICES 3 Number of simulated services

Architecture

.
├── bin                    # local build artifacts (generated binaries)
├── build                  # local infrastructure files (docker-compose)
├── cmd                    # executable entrypoints
│   ├── khipu              # ingest service (OTLP gRPC -> ClickHouse)
│   └── metricgen          # synthetic OTLP traffic generator
└── internal               # private application packages
    ├── ingest             # gRPC handler, mapping, and request orchestration
    ├── series             # deterministic SeriesID generation and cache
    ├── store              # storage interfaces
    │   └── clickhouse     # ClickHouse schema and insert implementation
    └── telemetry          # OpenTelemetry SDK bootstrap and exporters

Data Flow

flowchart LR
    MG[metricgen / OTLP client] --> H[ingest.Handler]
    H --> M[mapper + SeriesID]
    H --> C[series.Cache]
    H --> S[store/clickhouse]

    S --> MS[(metric_series)]
    S --> G[(otel_metrics_gauge)]
    S --> SU[(otel_metrics_sum)]
    G --> MV1[otel_metrics_gauge_1m_mv]
    MV1 --> G1[(otel_metrics_gauge_1m)]
    MS -. refresh .-> D[[metric_series_dict]]
    G -. dictGet by SeriesID .-> D
    G1 -. dictGet by SeriesID .-> D
    SU -. dictGet by SeriesID .-> D
Loading

Export Request Sequence

sequenceDiagram
    participant Client as OTLP Client / metricgen
    participant Handler as ingest.Handler
    participant Mapper as mapper
    participant Cache as series.Cache
    participant Store as MetricsStore
    participant CH as ClickHouse

    Client->>Handler: Export(ResourceMetrics)
    Handler->>Mapper: mapGaugeRows + mapSumRows
    Mapper-->>Handler: seriesRows + datapointRows

    Handler->>Handler: deduplicateSeries()
    Handler->>Cache: filterNewSeries()
    Cache-->>Handler: newSeries

    alt newSeries not empty
        Handler->>Store: InsertSeries(newSeries)
        Store->>CH: INSERT metric_series
    end

    par gauge
        Handler->>Store: InsertGauge(gaugeRows)
        Store->>CH: INSERT otel_metrics_gauge
        CH->>CH: MV aggregates into otel_metrics_gauge_1m
    and sum
        Handler->>Store: InsertSum(sumRows)
        Store->>CH: INSERT otel_metrics_sum
    end

    Handler->>Cache: Add(newSeries IDs)
    Handler-->>Client: ExportResponse OK
Loading

Data Model

metric_series (lookup table)

Stores series metadata (resource/scope/metric/attributes). Uses ReplacingMergeTree ordered by (MetricName, SeriesID) — rows are deduplicated on that composite key, keeping the latest LastSeenAt.

otel_metrics_gauge, otel_metrics_sum (fact tables)

Store:

  • SeriesID — reference to metric_series
  • timestamps
  • value
  • Attributes — datapoint-level attributes, inline with bloom filter indexes (ClickHouse dictionaries cannot store Map types; inline storage enables attribute filtering without a JOIN)

otel_metrics_gauge_1m + otel_metrics_gauge_1m_mv (gauge rollup)

Each insert into otel_metrics_gauge triggers the materialized view otel_metrics_gauge_1m_mv, which groups rows by minute and SeriesID into otel_metrics_gauge_1m (ValueSum, ValueCount; average = ValueSum / ValueCount). Use for dashboard-style time-range queries without scanning raw samples.

metric_series_dict (ClickHouse dictionary)

Provides query-time metadata lookup via dictGet without explicit JOINs.


Notes

  • the design is optimized for time-window queries
  • series metadata inserts are reduced by a bounded in-memory cache
  • ReplacingMergeTree in metric_series handles duplicate series metadata safely

About

PoC of OTLP-to-ClickHouse metrics pipeline using OpenTelemetry Collector SDK

Topics

Resources

License

Stars

Watchers

Forks

Contributors