|
1 | | -# Duckspan |
| 1 | +# duckspan |
2 | 2 |
|
3 | | -This repository is based on https://github.com/duckdb/extension-template, check it out if you want to build and ship your own DuckDB extension. |
| 3 | +Query OpenTelemetry data with SQL. Attach OTLP streams as DuckDB databases, read OTLP files. |
4 | 4 |
|
5 | | ---- |
| 5 | +## Quick Start |
6 | 6 |
|
7 | | -This extension, Duckspan, allow you to ... <extension_goal>. |
| 7 | +```sql |
| 8 | +-- Load extension |
| 9 | +LOAD duckspan; |
8 | 10 |
|
| 11 | +-- Attach live OTLP stream (starts gRPC receiver on port 4317) |
| 12 | +ATTACH 'otlp:localhost:4317' AS live (TYPE otlp); |
9 | 13 |
|
10 | | -## Building |
11 | | -### Managing dependencies |
12 | | -DuckDB extensions uses VCPKG for dependency management. Enabling VCPKG is very simple: follow the [installation instructions](https://vcpkg.io/en/getting-started) or just run the following: |
13 | | -```shell |
14 | | -git clone https://github.com/Microsoft/vcpkg.git |
15 | | -./vcpkg/bootstrap-vcpkg.sh |
16 | | -export VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmake |
| 14 | +-- Query accumulated telemetry |
| 15 | +SELECT * FROM live.traces; |
| 16 | +SELECT * FROM live.metrics; |
| 17 | +SELECT * FROM live.logs; |
| 18 | + |
| 19 | +-- Stop receiver |
| 20 | +DETACH live; |
| 21 | + |
| 22 | +-- Read OTLP files (JSON or protobuf, auto-detected) |
| 23 | +SELECT * FROM read_otlp('traces.jsonl'); |
| 24 | +SELECT * FROM read_otlp('s3://bucket/traces/*.pb'); |
17 | 25 | ``` |
18 | | -Note: VCPKG is only required for extensions that want to rely on it for dependency management. If you want to develop an extension without dependencies, or want to do your own dependency management, just skip this step. Note that the example extension uses VCPKG to build with a dependency for instructive purposes, so when skipping this step the build may not work without removing the dependency. |
19 | 26 |
|
20 | | -### Build steps |
21 | | -Now to build the extension, run: |
22 | | -```sh |
23 | | -make |
| 27 | +## Features |
| 28 | + |
| 29 | +**Live OTLP Streams** |
| 30 | +- `ATTACH` creates a gRPC receiver on specified port |
| 31 | +- Auto-creates tables: `{name}.traces`, `{name}.metrics`, `{name}.logs` |
| 32 | +- OpenTelemetry SDKs send data → DuckDB accumulates it |
| 33 | +- `DETACH` stops receiver and removes database |
| 34 | + |
| 35 | +**File Reading** |
| 36 | +- `read_otlp(filepath)` reads OTLP JSON/protobuf files |
| 37 | +- Supports JSON and protobuf formats (auto-detected) |
| 38 | +- Works with local files, S3, HTTP, Azure, GCS |
| 39 | +- Same schema as attached tables |
| 40 | + |
| 41 | +**Unified Schema** |
| 42 | +- All signal types use: `(timestamp TIMESTAMP, resource JSON, data JSON)` |
| 43 | +- Query with DuckDB's JSON functions |
| 44 | +- `resource` = service/host metadata |
| 45 | +- `data` = signal-specific payload |
| 46 | + |
| 47 | +## Usage |
| 48 | + |
| 49 | +### ATTACH/DETACH Lifecycle |
| 50 | + |
| 51 | +```sql |
| 52 | +-- Start receiver on port 4317 |
| 53 | +ATTACH 'otlp:localhost:4317' AS live (TYPE otlp); |
| 54 | + |
| 55 | +-- Different port |
| 56 | +ATTACH 'otlp:0.0.0.0:4318' AS metrics (TYPE otlp); |
| 57 | + |
| 58 | +-- Multiple simultaneous streams |
| 59 | +ATTACH 'otlp:localhost:4317' AS traces1 (TYPE otlp); |
| 60 | +ATTACH 'otlp:localhost:4318' AS traces2 (TYPE otlp); |
| 61 | + |
| 62 | +-- Cleanup |
| 63 | +DETACH traces1; |
| 64 | +DETACH traces2; |
24 | 65 | ``` |
25 | | -The main binaries that will be built are: |
26 | | -```sh |
27 | | -./build/release/duckdb |
28 | | -./build/release/test/unittest |
29 | | -./build/release/extension/duckspan/duckspan.duckdb_extension |
| 66 | + |
| 67 | +### Querying Traces |
| 68 | + |
| 69 | +```sql |
| 70 | +-- Extract service name and span details |
| 71 | +SELECT |
| 72 | + json_extract(resource, '$.service.name') as service, |
| 73 | + json_extract(data, '$.name') as span_name, |
| 74 | + json_extract(data, '$.traceId') as trace_id, |
| 75 | + timestamp |
| 76 | +FROM live.traces |
| 77 | +WHERE timestamp > NOW() - INTERVAL 1 HOUR; |
| 78 | + |
| 79 | +-- Find slow requests (duration > 1 second) |
| 80 | +SELECT |
| 81 | + json_extract(resource, '$.service.name') as service, |
| 82 | + json_extract(data, '$.name') as operation, |
| 83 | + (json_extract(data, '$.endTimeUnixNano')::BIGINT - |
| 84 | + json_extract(data, '$.startTimeUnixNano')::BIGINT) / 1000000 as duration_ms |
| 85 | +FROM live.traces |
| 86 | +WHERE duration_ms > 1000 |
| 87 | +ORDER BY duration_ms DESC; |
| 88 | + |
| 89 | +-- Error rate by service |
| 90 | +SELECT |
| 91 | + json_extract(resource, '$.service.name') as service, |
| 92 | + COUNT(*) as total_spans, |
| 93 | + SUM(CASE WHEN json_extract(data, '$.status.code') != 0 THEN 1 ELSE 0 END) as errors, |
| 94 | + (errors::FLOAT / total_spans * 100)::INT as error_pct |
| 95 | +FROM live.traces |
| 96 | +WHERE timestamp > NOW() - INTERVAL 15 MINUTES |
| 97 | +GROUP BY service; |
30 | 98 | ``` |
31 | | -- `duckdb` is the binary for the duckdb shell with the extension code automatically loaded. |
32 | | -- `unittest` is the test runner of duckdb. Again, the extension is already linked into the binary. |
33 | | -- `duckspan.duckdb_extension` is the loadable binary as it would be distributed. |
34 | 99 |
|
35 | | -## Running the extension |
36 | | -To run the extension code, simply start the shell with `./build/release/duckdb`. |
| 100 | +### Reading Files |
| 101 | + |
| 102 | +```sql |
| 103 | +-- Local files |
| 104 | +SELECT COUNT(*) FROM read_otlp('test/data/traces.jsonl'); |
| 105 | + |
| 106 | +-- S3 (with DuckDB's S3 support) |
| 107 | +SELECT * FROM read_otlp('s3://bucket/telemetry/*.jsonl'); |
| 108 | + |
| 109 | +-- HTTP |
| 110 | +SELECT * FROM read_otlp('https://example.com/traces.jsonl'); |
| 111 | + |
| 112 | +-- Protobuf (auto-detected) |
| 113 | +SELECT * FROM read_otlp('traces.pb'); |
37 | 114 |
|
38 | | -Now we can use the features from the extension directly in DuckDB. The template contains a single scalar function `duckspan()` that takes a string arguments and returns a string: |
| 115 | +-- Verify schema |
| 116 | +SELECT |
| 117 | + typeof(timestamp) as ts_type, |
| 118 | + typeof(resource) as resource_type, |
| 119 | + typeof(data) as data_type |
| 120 | +FROM read_otlp('traces.jsonl') |
| 121 | +LIMIT 1; |
39 | 122 | ``` |
40 | | -D select duckspan('Jane') as result; |
41 | | -┌───────────────┐ |
42 | | -│ result │ |
43 | | -│ varchar │ |
44 | | -├───────────────┤ |
45 | | -│ Duckspan Jane 🐥 │ |
46 | | -└───────────────┘ |
| 123 | + |
| 124 | +### Persisting Stream Data |
| 125 | + |
| 126 | +```sql |
| 127 | +-- Attach and accumulate data |
| 128 | +ATTACH 'otlp:localhost:4317' AS live (TYPE otlp); |
| 129 | + |
| 130 | +-- Let telemetry flow in for a while... |
| 131 | + |
| 132 | +-- Save to permanent tables |
| 133 | +CREATE TABLE archive_traces AS SELECT * FROM live.traces; |
| 134 | +CREATE TABLE archive_metrics AS SELECT * FROM live.metrics; |
| 135 | + |
| 136 | +-- Stop receiver |
| 137 | +DETACH live; |
| 138 | + |
| 139 | +-- Query archived data |
| 140 | +SELECT * FROM archive_traces WHERE timestamp > NOW() - INTERVAL 1 DAY; |
47 | 141 | ``` |
48 | 142 |
|
49 | | -## Running the tests |
50 | | -Different tests can be created for DuckDB extensions. The primary way of testing DuckDB extensions should be the SQL tests in `./test/sql`. These SQL tests can be run using: |
51 | | -```sh |
52 | | -make test |
| 143 | +## Data Model |
| 144 | + |
| 145 | +All signal types (traces, metrics, logs) use the same schema: |
| 146 | + |
| 147 | +| Column | Type | Description | |
| 148 | +|--------|------|-------------| |
| 149 | +| `timestamp` | TIMESTAMP | When the event occurred | |
| 150 | +| `resource` | JSON | Service/host metadata | |
| 151 | +| `data` | JSON | Signal-specific payload | |
| 152 | + |
| 153 | +**Example Row:** |
| 154 | +```json |
| 155 | +{ |
| 156 | + "timestamp": "2024-01-15 10:30:00", |
| 157 | + "resource": { |
| 158 | + "service.name": "api-server", |
| 159 | + "host.name": "prod-01" |
| 160 | + }, |
| 161 | + "data": { |
| 162 | + "traceId": "5B8EFFF798038103D269B633813FC60C", |
| 163 | + "spanId": "EEE19B7EC3C1B174", |
| 164 | + "name": "GET /users", |
| 165 | + "startTimeUnixNano": "1640000000000000000", |
| 166 | + "endTimeUnixNano": "1640000000100000000", |
| 167 | + "attributes": { |
| 168 | + "http.method": "GET", |
| 169 | + "http.url": "/users", |
| 170 | + "http.status_code": 200 |
| 171 | + }, |
| 172 | + "status": {"code": 0} |
| 173 | + } |
| 174 | +} |
53 | 175 | ``` |
54 | 176 |
|
55 | | -### Installing the deployed binaries |
56 | | -To install your extension binaries from S3, you will need to do two things. Firstly, DuckDB should be launched with the |
57 | | -`allow_unsigned_extensions` option set to true. How to set this will depend on the client you're using. Some examples: |
| 177 | +## Building |
| 178 | + |
| 179 | +### Prerequisites |
58 | 180 |
|
59 | | -CLI: |
60 | | -```shell |
61 | | -duckdb -unsigned |
| 181 | +Install VCPKG for dependency management: |
| 182 | + |
| 183 | +```bash |
| 184 | +git clone https://github.com/Microsoft/vcpkg.git |
| 185 | +./vcpkg/bootstrap-vcpkg.sh |
| 186 | +export VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmake |
62 | 187 | ``` |
63 | 188 |
|
64 | | -Python: |
65 | | -```python |
66 | | -con = duckdb.connect(':memory:', config={'allow_unsigned_extensions' : 'true'}) |
| 189 | +### Build |
| 190 | + |
| 191 | +Use ninja for performance: |
| 192 | + |
| 193 | +```bash |
| 194 | +GEN=ninja make |
67 | 195 | ``` |
68 | 196 |
|
69 | | -NodeJS: |
70 | | -```js |
71 | | -db = new duckdb.Database(':memory:', {"allow_unsigned_extensions": "true"}); |
| 197 | +Builds: |
| 198 | +- `./build/release/duckdb` - DuckDB shell with extension loaded |
| 199 | +- `./build/release/test/unittest` - Test runner |
| 200 | +- `./build/release/extension/duckspan/duckspan.duckdb_extension` - Loadable extension |
| 201 | + |
| 202 | +### Testing |
| 203 | + |
| 204 | +```bash |
| 205 | +# SQL tests |
| 206 | +make test |
| 207 | + |
| 208 | +# OTLP integration test (requires uv) |
| 209 | +uv run test/python/test_otlp_export.py |
| 210 | + |
| 211 | +# Or via make |
| 212 | +make test-otlp-export |
72 | 213 | ``` |
73 | 214 |
|
74 | | -Secondly, you will need to set the repository endpoint in DuckDB to the HTTP url of your bucket + version of the extension |
75 | | -you want to install. To do this run the following SQL query in DuckDB: |
76 | | -```sql |
77 | | -SET custom_extension_repository='bucket.s3.eu-west-1.amazonaws.com/<your_extension_name>/latest'; |
| 215 | +## Development |
| 216 | + |
| 217 | +### Pre-commit Hooks |
| 218 | + |
| 219 | +Uses `uv` for Python tooling: |
| 220 | + |
| 221 | +```bash |
| 222 | +# Install hooks |
| 223 | +uvx --from pre-commit pre-commit install |
| 224 | + |
| 225 | +# Run manually |
| 226 | +uvx --from pre-commit pre-commit run --all-files |
78 | 227 | ``` |
79 | | -Note that the `/latest` path will allow you to install the latest extension version available for your current version of |
80 | | -DuckDB. To specify a specific version, you can pass the version instead. |
81 | 228 |
|
82 | | -After running these steps, you can install and load your extension using the regular INSTALL/LOAD commands in DuckDB: |
| 229 | +Hooks: |
| 230 | +- clang-format (C++ formatting) |
| 231 | +- black (Python formatting) |
| 232 | +- cmake-format/cmake-lint |
| 233 | + |
| 234 | +### Code Formatting |
| 235 | + |
| 236 | +```bash |
| 237 | +# Check format |
| 238 | +make format-check |
| 239 | + |
| 240 | +# Auto-fix |
| 241 | +make format-fix |
| 242 | +``` |
| 243 | + |
| 244 | +## Architecture |
| 245 | + |
| 246 | +- **Extension Type**: Storage extension registered for `TYPE otlp` |
| 247 | +- **ATTACH Handler**: Parses connection string, starts gRPC server, creates tables |
| 248 | +- **gRPC Receiver**: Implements OTLP collector service endpoints (traces/metrics/logs) |
| 249 | +- **Table Functions**: `read_otlp()` streams OTLP files with format auto-detection |
| 250 | +- **Data Conversion**: Protobuf → JSON for SQL queryability |
| 251 | + |
| 252 | +### Dependencies (via VCPKG) |
| 253 | +- gRPC (OTLP receiver) |
| 254 | +- Protobuf (wire format) |
| 255 | +- OpenSSL (gRPC dependency) |
| 256 | + |
| 257 | +## Limitations |
| 258 | + |
| 259 | +**Data Retention**: Attached streams accumulate data in memory indefinitely. Manual cleanup required: |
| 260 | + |
83 | 261 | ```sql |
84 | | -INSTALL duckspan |
85 | | -LOAD duckspan |
| 262 | +-- Option 1: Periodic DELETE |
| 263 | +DELETE FROM live.traces WHERE timestamp < NOW() - INTERVAL 1 HOUR; |
| 264 | + |
| 265 | +-- Option 2: Periodic persist-and-detach |
| 266 | +CREATE TABLE archive AS SELECT * FROM live.traces; |
| 267 | +DETACH live; |
| 268 | +ATTACH 'otlp:localhost:4317' AS live (TYPE otlp); |
86 | 269 | ``` |
| 270 | + |
| 271 | +Future versions may add automatic TTL or persistent backing. |
| 272 | + |
| 273 | +## References |
| 274 | + |
| 275 | +- [OpenTelemetry Protocol (OTLP)](https://opentelemetry.io/docs/specs/otlp/) |
| 276 | +- [DuckDB Extensions](https://duckdb.org/docs/extensions/overview) |
| 277 | +- [Extension Template](https://github.com/duckdb/extension-template) |
0 commit comments