Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4ed99ac
feat(sender): align OTLP exporter configuration with OTel spec
kyletaylored Apr 28, 2026
1127483
fix(metrics): serialize aggregationTemporality as integer enum
kyletaylored Apr 28, 2026
fe17536
feat(examples): fix basic example and add direct_otlp example
kyletaylored Apr 28, 2026
3ee78a4
chore: update CI, docs, and project metadata
kyletaylored Apr 28, 2026
02e296f
fix(metrics): correct aggregationTemporality enum values
kyletaylored Apr 28, 2026
7649eb3
feat(proto): add full protobuf/nanopb encoding path for all three OTL…
kyletaylored Apr 28, 2026
4c69ba1
feat(examples): add proto_otlp example and declare examples in librar…
kyletaylored Apr 28, 2026
7b9491d
ci: add workflow_dispatch trigger for manual test runs
kyletaylored Apr 28, 2026
0d0d91d
ci: add validate workflow with workflow_dispatch trigger
kyletaylored Apr 28, 2026
4483e43
fix(ci): prevent registry library from shadowing local source
kyletaylored Apr 29, 2026
590742d
fix(ci): fully specify proto envs instead of using extends
kyletaylored Apr 29, 2026
ffe2fb9
fix(proto): use relative includes in .pb.inc files
kyletaylored Apr 29, 2026
f61796d
fix(proto): use relative includes in .pb.h cross-references
kyletaylored Apr 29, 2026
67eb345
fix(proto): include OtelMetrics.h and OtelLogger.h in OtelProtoEncode…
kyletaylored Apr 29, 2026
da618c4
fix(warnings): remove unused static functions in proto build path
kyletaylored Apr 29, 2026
7551325
feat(test): add native unit test skeleton for OTLP payload validation
kyletaylored Apr 29, 2026
f3d5b0b
refactor(proto): replace local pb files with aodtorusan/opentelemetry…
kyletaylored Apr 29, 2026
63adcb8
chore(config): add opentelemetry_proto to all envs, restore comments
kyletaylored Apr 29, 2026
a05cd1c
fix(ci): rename test suite dir to test_otlp so PlatformIO discovers it
kyletaylored Apr 29, 2026
a83291e
fix(native-tests): resolve String type errors in host GCC build
kyletaylored Apr 29, 2026
a466fac
This command is deprecated and will be removed in the next releases.
kyletaylored Apr 29, 2026
40c9b38
fix(native-tests): add missing Arduino polyfills to host stub
kyletaylored Apr 29, 2026
916d86d
fix(native-tests): add String::concat and Stream::readBytes to stub
kyletaylored Apr 29, 2026
6261182
fix(tests): get all 13 native Unity tests passing
kyletaylored Apr 29, 2026
ccc3da7
Merge branch 'main' into otlp-exporter
kyletaylored Apr 29, 2026
0fe3e27
Merge pull request #1 from kyletaylored/otlp-exporter
kyletaylored Apr 29, 2026
09d473f
docs: add Doxygen docstrings to reach 80% coverage threshold
kyletaylored Apr 29, 2026
854b2c4
Remove broken OTel type aliases that referenced nonexistent OTelLogge…
kyletaylored May 4, 2026
3171866
fix: address CodeRabbit PR review findings
kyletaylored May 6, 2026
0bda033
Add OTelSender::addHeader() runtime API for backend-specific headers
kyletaylored May 6, 2026
6c8f9ab
fix(sender): address CodeRabbit findings on OtelSender.cpp
kyletaylored May 10, 2026
202ec21
fix(sender): use binary buffer for queue payloads + bounds-check tuna…
kyletaylored May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,32 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: # allow manual runs from the GitHub Actions tab

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install PlatformIO
run: python -m pip install -U platformio

# Make repo Variables + Secrets available to all subsequent steps
# Expose repo variables and secrets as environment variables for PlatformIO
# sysenv.* references in platformio.ini build_flags pick these up at build time.
- name: Export env for build
run: |
echo "::add-mask::${{ secrets.WIFI_PASS }}"
{
echo "WIFI_SSID=${{ vars.WIFI_SSID }}"
echo "WIFI_PASS=${{ secrets.WIFI_PASS }}"
echo "OTEL_COLLECTOR_HOST=${{ vars.OTEL_COLLECTOR_HOST }}"
echo "OTEL_COLLECTOR_PORT=${{ vars.OTEL_COLLECTOR_PORT }}"
echo "OTEL_EXPORTER_OTLP_ENDPOINT=${{ vars.OTEL_EXPORTER_OTLP_ENDPOINT || '' }}"
echo "OTEL_SERVICE_NAME=${{ vars.OTEL_SERVICE_NAME }}"
echo "OTEL_SERVICE_NAMESPACE=${{ vars.OTEL_SERVICE_NAMESPACE }}"
echo "OTEL_SERVICE_VERSION=${{ vars.OTEL_SERVICE_VERSION }}"
Expand All @@ -38,14 +39,63 @@ jobs:
} >> "$GITHUB_ENV"

- name: PlatformIO Update
run: pio update
run: pio pkg update

- name: Build for ESP32 (esp32dev)
# Remove any cached registry version so --lib "." always wins and we
# compile the local source, not the published package at the same version.
- name: Flush cached library
run: pio pkg uninstall --global --library "otel-embedded-cpp" 2>/dev/null || true

# ── Native unit tests (host GCC, no hardware required) ──────────────────
# Compiles and runs test/native/test_otlp.cpp against a fake OTelSender
# that captures emitted JSON so tests can assert on payload structure.
# GCC is pre-installed on ubuntu-latest; no extra toolchain step needed.

- name: Run native unit tests
run: pio test -e native

# ── src/main.cpp (existing integration test) ───────────────────────────

- name: Build src for ESP32 (esp32dev)
run: platformio ci src/main.cpp --project-conf platformio.ini --lib "." -e esp32dev

- name: Build for Pico W (rpipicow)
- name: Build src for Pico W (rpipicow)
run: platformio ci src/main.cpp --project-conf platformio.ini --lib "." -e rpipicow

- name: Build for ESP8266 (esp8266 d1_mini)
- name: Build src for ESP8266 (d1_mini)
run: platformio ci src/main.cpp --project-conf platformio.ini --lib "." -e esp8266

# ── examples/basic (minimal heartbeat, collector-based setup) ────────────

- name: Build basic example for ESP32 (esp32dev)
run: platformio ci examples/basic/main.cpp --project-conf platformio.ini --lib "." -e esp32dev

- name: Build basic example for Pico W (rpipicow)
run: platformio ci examples/basic/main.cpp --project-conf platformio.ini --lib "." -e rpipicow

- name: Build basic example for ESP8266 (d1_mini)
run: platformio ci examples/basic/main.cpp --project-conf platformio.ini --lib "." -e esp8266

# ── examples/direct_otlp (validates OTLP exporter configuration) ─────────

- name: Build direct_otlp example for ESP32 (esp32dev)
run: platformio ci examples/direct_otlp/main.cpp --project-conf platformio.ini --lib "." -e esp32dev

- name: Build direct_otlp example for Pico W (rpipicow)
run: platformio ci examples/direct_otlp/main.cpp --project-conf platformio.ini --lib "." -e rpipicow

- name: Build direct_otlp example for ESP8266 (d1_mini)
run: platformio ci examples/direct_otlp/main.cpp --project-conf platformio.ini --lib "." -e esp8266

# ── examples/proto_otlp (validates protobuf encoding path) ───────────────
# Uses dedicated *-proto envs in platformio.ini that inherit their base
# env and add -DOTEL_EXPORTER_OTLP_PROTOCOL=...HTTP_PROTOBUF.

- name: Build proto_otlp example for ESP32 (esp32dev-proto)
run: platformio ci examples/proto_otlp/main.cpp --project-conf platformio.ini --lib "." -e esp32dev-proto

- name: Build proto_otlp example for Pico W (rpipicow-proto)
run: platformio ci examples/proto_otlp/main.cpp --project-conf platformio.ini --lib "." -e rpipicow-proto

- name: Build proto_otlp example for ESP8266 (esp8266-proto)
run: platformio ci examples/proto_otlp/main.cpp --project-conf platformio.ini --lib "." -e esp8266-proto
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@

# Environment secrets
.env

# Local dev
.claude
.vscode
134 changes: 72 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ This removes any blocking code and ensures that the HTTP POST call does not inte

```ini
build_flags =
-DWIFI_SSID="${sysenv.OTEL_WIFI_SSID}"
-DWIFI_PASS="${sysenv.OTEL_WIFI_PASS}"
-DOTEL_COLLECTOR_HOST="${sysenv.OTEL_COLLECTOR_HOST}"
-DOTEL_COLLECTOR_PORT=${sysenv.OTEL_COLLECTOR_PORT}
-DWIFI_SSID="${sysenv.WIFI_SSID}"
-DWIFI_PASS="${sysenv.WIFI_PASS}"
-DOTEL_EXPORTER_OTLP_ENDPOINT="\"${sysenv.OTEL_EXPORTER_OTLP_ENDPOINT}\""
-DOTEL_EXPORTER_OTLP_HEADERS="\"${sysenv.OTEL_EXPORTER_OTLP_HEADERS}\""
-DOTEL_SERVICE_NAME="${sysenv.OTEL_SERVICE_NAME}"
-DOTEL_SERVICE_NAMESPACE="${sysenv.OTEL_SERVICE_NAMESPACE}"
-DOTEL_SERVICE_VERSION="${sysenv.OTEL_SERVICE_VERSION}"
Expand All @@ -59,10 +59,9 @@ This removes any blocking code and ensures that the HTTP POST call does not inte
3. **(Optional)** Use a `.env` file and load it into your shell:

```dotenv
OTEL_WIFI_SSID=default
OTEL_WIFI_PASS=default
OTEL_COLLECTOR_HOST=192.168.1.100
OTEL_COLLECTOR_PORT=4318
WIFI_SSID=default
WIFI_PASS=default
OTEL_EXPORTER_OTLP_ENDPOINT=http://192.168.1.100:4318
OTEL_SERVICE_NAME=demo_service
OTEL_SERVICE_NAMESPACE=demo_namespace
OTEL_SERVICE_VERSION=v1.0.0
Expand All @@ -87,108 +86,119 @@ This removes any blocking code and ensures that the HTTP POST call does not inte

#if defined(ESP32)
#include <WiFi.h>
#include <esp_system.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ARDUINO_ARCH_RP2040)
// Earle Philhower’s Arduino-Pico core exposes a WiFi.h for Pico W
#include <WiFi.h>
#else
#error "This example targets ESP32, ESP8266, or RP2040 (Pico W) with WiFi."
#endif

// ---------------------------------------------------------
// Import Open Telemetry Libraries
// ---------------------------------------------------------
#include "OtelDefaults.h"
#include "OtelSender.h"
#include "OtelTracer.h"
#include "OtelLogger.h"
#include "OtelMetrics.h"
#include "OtelDebug.h"

static constexpr uint32_t HEARTBEAT_INTERVAL = 5000;

void setup() {
Serial.begin(115200);

// Seed PRNG (fresh trace IDs each boot)
#if defined(ARDUINO_ARCH_ESP32)
randomSeed(esp_random());
#else
randomSeed(micros());
#endif

// Connect to WiFi
WiFi.begin(OTEL_WIFI_SSID, OTEL_WIFI_PASS);
#if defined(ARDUINO_ARCH_ESP32)
randomSeed(esp_random());
#else
randomSeed(micros());
#endif

// Connect to Wi-Fi
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); }

// Sync NTP
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
while (time(nullptr) < 1609459200UL) { delay(500); }

// Initialise Logger & Tracer

// Set the defaults for the resources
auto &res = OTel::defaultResource();
res.set("service", OTEL_SERVICE_NAME);
res.set("service.name", OTEL_SERVICE_NAME);
res.set("service.namespace", OTEL_SERVICE_NAMESPACE);
res.set("service.instance.id", OTEL_SERVICE_INSTANCE);
res.set("host.name", "my-embedded device");

// Setup our tracing engine
OTel::Tracer::begin("otel-embedded", "1.0.1");

// Make sure that we start with empty trace and span ID's
OTel::currentTraceContext().traceId = "";
OTel::currentTraceContext().spanId = "";

// Setup the metrics engine
OTel::Metrics::begin("otel-embedded", "1.0.1");
OTel::Metrics::setDefaultMetricLabel("device.role", "test-device");
OTel::Metrics::setDefaultMetricLabel("device.id", "device-chip-id-or-mac");
// Initialise tracer and metrics (scopeName, scopeVersion)
OTel::Tracer::begin("otel-embedded", "1.0.0");
OTel::Metrics::begin("otel-embedded", "1.0.0");

// On RP2040, start the core-1 async send worker after Wi-Fi is ready
#if defined(ARDUINO_ARCH_RP2040)
OTelSender::beginAsyncWorker();
#endif
}

void loop() {
// Heartbeat trace
auto span = OTel::Tracer::startSpan("heartbeat");

OTel::Logger::logInfo("Heartbeat event");
static OTel::OTelGauge gauge("heartbeat.gauge", "1");
gauge.set(1.0f);
span.end();

OTel::Metrics::gauge("heartbeat.uptime_seconds",
static_cast<double>(millis() / 1000), "s");

span.end();
delay(HEARTBEAT_INTERVAL);
}
```

This will emit:

* **Traces** for each `startSpan("heartbeat")`
* **Logs** with `service.*` resource attributes
* **Metrics** via `OTelGauge`, `OTelCounter` and `OTelHistogram`
* **Logs** correlated to the active span via `traceId`/`spanId`
* **Metrics** as a gauge via `Metrics::gauge()`

All data is sent over OTLP/HTTP to the configured collector.

---

## 🛠 Configuration Macros

Override defaults in `OtelDefaults.h` or via `-D` flags:
Set via `-D` flags in `platformio.ini` `build_flags`.

### Endpoint

| Macro | Default | Description |
| ------------------------------------------ | ------------------------------ | ----------- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | *(empty)* | Base URL for all signals; `/v1/traces`, `/v1/metrics`, `/v1/logs` are appended automatically. Follows the [OTel exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). Takes priority over `OTEL_COLLECTOR_BASE_URL`. |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | *(empty)* | Per-signal endpoint override, used verbatim (no path appended). Overrides `OTEL_EXPORTER_OTLP_ENDPOINT` for logs. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | *(empty)* | Same, for traces. |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | *(empty)* | Same, for metrics. |
| `OTEL_COLLECTOR_BASE_URL` | `http://192.168.8.50:4318` | Legacy base URL fallback. Prefer `OTEL_EXPORTER_OTLP_ENDPOINT` for new setups. |

### Headers & Authentication

| Macro | Default | Description |
| ------------------------------------------ | --------- | ----------- |
| `OTEL_EXPORTER_OTLP_HEADERS` | *(empty)* | Comma-separated `key=value` headers added to every request. Values containing commas must be percent-encoded. Example: `"dd-api-key=abc123"` |
| `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | *(empty)* | Per-signal header overrides, merged on top of `OTEL_EXPORTER_OTLP_HEADERS`. |
| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | *(empty)* | Same, for traces. |
| `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | *(empty)* | Same, for metrics. |

### TLS

| Macro | Default | Description |
| -------------------- | ------- | ----------- |
| `OTEL_TLS_INSECURE` | `1` | When `1`, HTTPS connections skip certificate validation. Set to `0` for strict validation (requires a CA cert — see `OtelSender.h`). |

### Service identity

| Macro | Default | Description |
| ------------------------ | ------------------ | ----------------------------------------------- |
| `WIFI_SSID` | `"default"` | Wi‑Fi SSID |
| `WIFI_PASS` | `"default"` | Wi‑Fi password |
| `OTEL_COLLECTOR_BASE_URL`| `Null` | The base URL (http://192.168.8.10:4318) of the otel collector |
| `OTEL_SERVICE_NAME` | `"demo_service"` | Name of your service |
| `OTEL_SERVICE_NAMESPACE` | `"demo_namespace"` | Service namespace |
| `OTEL_SERVICE_VERSION` | `"v1.0.0"` | Semantic version |
| `OTEL_SERVICE_INSTANCE` | `"instance-1"` | Unique instance ID |
| `OTEL_DEPLOY_ENV` | `"dev"` | Deployment environment (e.g. `prod`, `staging`) |
| `OTEL_WORKER_BURST` | `16` | The number of telemetry messages to process at a time |
| `OTEL_WORKER_SLEEP_MS` | `0` | How long to sleep between processing messages (0 is instant) |
| `OTEL_QUEUE_CAPACITY` | `128` | The maximum number of telemetry messages we can store before we start to drop data |
| `DEBUG` | `Null` | Print verbose messages including OTEL Payload to the serial port |
| `OTEL_SERVICE_NAME` | `"embedded-service"` | Name of your service |
| `OTEL_SERVICE_INSTANCE` | chip ID | Unique instance identifier |
| `OTEL_HOST_NAME` | `"ESP-<chipid>"` | Host name reported in resource attributes |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Send behaviour

| Macro | Default | Description |
| --------------------- | ------- | ----------- |
| `OTEL_SEND_ENABLE` | `1` | Set to `0` to compile out all network sends (useful for latency benchmarking). |
| `OTEL_WORKER_BURST` | `8` | Items dequeued and sent per worker loop iteration (RP2040). |
| `OTEL_WORKER_SLEEP_MS`| `0` | Delay between worker iterations in ms. |
| `OTEL_QUEUE_CAPACITY` | `16` | SPSC queue depth for the RP2040 core-1 sender. |
| `DEBUG` | *(unset)* | Print verbose output including OTLP payloads to Serial. |


---
Expand Down
42 changes: 42 additions & 0 deletions examples/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# basic example

Minimal working sketch that sends a heartbeat trace, a correlated log, and a
gauge metric on a fixed interval.

## What it does

Every 5 seconds the main loop:

1. Opens a trace span called `heartbeat`
2. Emits an `INFO` log correlated to that span (the `traceId` and `spanId` are
attached automatically)
3. Records a gauge metric (`heartbeat.uptime_seconds`)
4. Closes the span, which triggers the OTLP send

## Configuration

Set these via `-D` flags in your `platformio.ini` `build_flags`:

| Flag | Purpose |
|---|---|
| `WIFI_SSID` / `WIFI_PASS` | Wi-Fi credentials |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL of your collector, e.g. `http://192.168.1.100:4318` |
| `OTEL_SERVICE_NAME` | Service name reported in all telemetry |

See the root [README](../../README.md) for the full list of configuration flags.

## Running

```ini
; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps = https://github.com/proffalken/otel-embedded-cpp.git#main
build_flags =
-DWIFI_SSID="\"your-ssid\""
-DWIFI_PASS="\"your-password\""
-DOTEL_EXPORTER_OTLP_ENDPOINT="\"http://192.168.1.100:4318\""
-DOTEL_SERVICE_NAME="\"my-device\""
```
Loading