Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Discarded Threads

(None yet)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Open Questions

1. Should we generate protox modules from .proto files or hand-write them?
- The opentelemetry-proto directory exists but appears empty
- Current .pb.ex files were generated by protoc-gen-elixir (protobuf library's code generator)
- protox has its own code generator (protox.generate) that could be used

2. Version bump: should this be a minor or major version bump?
- The public API doesn't change, only the serialization library
- But the protobuf message structs are part of the API surface (used in tests)
43 changes: 43 additions & 0 deletions .agent-tasks/2026-03-19-protox-otel-metric-exporter/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Progress Log

## 2026-03-19

### Initial setup
- Cloned repo to ~/code/electric-sql/worktrees/elixir-otel-metric-exporter/protox-migration
- Branch: protox-migration (from main @ d579d0f)
- Read all source files and .pb.ex files to understand the codebase
- GH token lacks 'project' scope for board mutations - left comment on issue instead
- Created task files in .agent-tasks/2026-03-19-protox-otel-metric-exporter/

### Analysis
Key protobuf touchpoints identified:
1. 6 `.pb.ex` files using `use Protobuf` macro - need complete rewrite for protox
2. `otel_api.ex:62-74` - `Protobuf.encode_to_iodata/1` and `Protobuf.EncodeError`
3. `protocol.ex:64-71` - `SeverityNumber.value(:ATOM)` enum helper
4. Tests use `Protobuf.decode/2` for decoding in assertions
5. `metric_store_test.exs` uses `ExportMetricsServiceRequest.decode(body)` directly

### Planning phase
- Spawned parallel subagents: (a) protox API research + plan, (b) protobuf removal experiment
- Planner researched protox docs extensively, identified key API differences
- Experimenter confirmed: removing protobuf breaks all 6 .pb.ex files (37 `use Protobuf` stmts),
plus 7 direct `Protobuf.*` call sites across lib/ and test/
- Key insight: oneof fields and enum atoms are compatible between libraries

### Implementation
- Used `mix protox.generate` with `--namespace=OtelMetricExporter` to generate from .proto files
- All 36 modules regenerated with matching names
- Key changes:
- `Protobuf.encode_to_iodata/1` → `Protox.encode!/1` (returns `{iodata, size}` tuple)
- `Protobuf.decode/2` → `Protox.decode!/2`
- `Module.decode/1` → `Module.decode!/1`
- `SeverityNumber.value(:ATOM)` → just `:ATOM` (protox enum fields accept atoms)
- `Protobuf.EncodeError` → `Protox.EncodingError`
- Protox is stricter: string fields can't be nil, must be ""
- nil metric values converted to 0/0.0 defaults
- All 44 tests pass, clean compilation with --warnings-as-errors
- Version bumped to 0.5.0

### PR & Review
- Pushed branch and opened PR with `claude` label
- Spawned review subagent
21 changes: 21 additions & 0 deletions .agent-tasks/2026-03-19-protox-otel-metric-exporter/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Task: Replace protobuf with protox in otel_metric_exporter

## Background
The `otel_metric_exporter` library currently depends on `protobuf` (elixir-protobuf/protobuf) for encoding/decoding OpenTelemetry protobuf messages. This creates a version pin issue in dependent projects (electric/stratovolt). See https://github.com/electric-sql/electric/issues/4018.

## Goal
Replace the `protobuf` dependency with `protox` (https://github.com/ahamez/protox), a pure-Elixir Protocol Buffers library that doesn't require pinning specific versions.

## Scope
1. Replace `{:protobuf, "~> 0.15"}` with `{:protox, "~> ..."}` in mix.exs
2. Rewrite all `.pb.ex` files (6 files under `lib/otel_metric_exporter/opentelemetry/proto/`) to use protox-style module definitions
3. Update all call sites that use `Protobuf.encode_to_iodata/1`, `Protobuf.decode/2`, and enum `value/1` functions
4. Ensure all tests pass
5. Bump the library version appropriately

## Key files using protobuf
- `lib/otel_metric_exporter/otel_api.ex` - `Protobuf.encode_to_iodata/1` and `Protobuf.EncodeError`
- `lib/otel_metric_exporter/protocol.ex` - `SeverityNumber.value/1` for enum mapping
- `lib/otel_metric_exporter/metric_store.ex` - `ExportMetricsServiceRequest.decode/1` (in tests via the module)
- Test files - `Protobuf.decode/2` calls for decoding responses
- 6 `.pb.ex` files - protobuf message definitions using `use Protobuf`
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Discarded Threads

None at this time.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Open Questions

None at this time.
31 changes: 31 additions & 0 deletions .agent-tasks/2026-03-20--1--protox-otel-repeat/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Progress Log

## 2026-03-20

- Reviewed current state of protox-migration branch
- Identified three feedback items to address

### P1: nil measurement handling
- Root cause: `extract_measurement` returns nil when measurement key not in event map
- nil flows through to `convert_value(nil, :int)` which emits `{:as_int, 0}`
- Fix: added nil check in `handle_metric/4` to skip writing when value is nil
- Removed the `convert_value(nil, ...)` fallback clauses
- Added test case verifying nil measurements are silently skipped

### P2: decode/1 return shape
- Old protobuf library's `decode/1` returned struct directly
- Protox-generated code wrapped result in `{:ok, struct}`
- Changed all 32 `decode/1` functions to return struct directly (matching old API)
- No callers in the codebase use `decode/1` (all use `decode!/1`), but preserving API for external users

### P3: Large generated file analysis
- File had 36 modules totaling 10,643 lines (after P2 changes)
- 22 modules are directly used by the library
- Transitive dependency analysis revealed 7 additional modules needed (Exemplar, ExponentialHistogram, ExponentialHistogramDataPoint, ExponentialHistogramDataPoint.Buckets, Summary, SummaryDataPoint, SummaryDataPoint.ValueAtQuantile)
- 8 modules safely removable: LogRecordFlags, DataPointFlags, ExportLogsPartialSuccess, ExportLogsServiceResponse, ExportMetricsPartialSuccess, ExportMetricsServiceResponse, LogsData, MetricsData
- Removed 1,459 lines (13.8% reduction), file now ~9,176 lines
- Further reduction would require modifying used modules to remove references to transitively-needed unused modules (e.g., removing Exemplar references from NumberDataPoint), which risks breaking proto wire compatibility

### Results
- All 45 tests pass (44 original + 1 new)
- Pushed changes to protox-migration branch
9 changes: 9 additions & 0 deletions .agent-tasks/2026-03-20--1--protox-otel-repeat/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Repeat Task: Address PR #30 review feedback

Session: 2026-03-20--1--protox-otel-repeat

## Feedback items

1. P1: Skip nil measurements instead of coercing to zero
2. P2: Preserve decode/1 return shape (return struct directly, not {:ok, struct})
3. Large generated file: analyze unused modules, remove them to trim the file
17 changes: 17 additions & 0 deletions .agent-tasks/2026-03-20--1--protox-otel-repeat/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Task: Address PR #30 review feedback for protox migration

## P1: nil measurement handling
- `extract_measurement` can return nil when the measurement key doesn't exist
- Currently, `convert_value(nil, :int)` returns `{:as_int, 0}` which corrupts metrics
- Fix: skip the metric write when value is nil

## P2: decode/1 return shape
- Old protobuf library's decode/1 returned struct directly
- New protox-generated decode/1 returns {:ok, struct}
- Fix: change decode/1 to return struct directly for API compatibility

## P3: Large generated file
- 10,739 lines in a single file with 36 modules
- Only 22 modules are actually used by the library
- 14 modules (4,020 lines, 37.4%) are unused
- Plan: remove unused modules to reduce file to ~6,700 lines
9 changes: 6 additions & 3 deletions lib/otel_metric_exporter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,13 @@ defmodule OtelMetricExporter do
for metric <- metrics do
if is_nil(metric.keep) || metric.keep.(metadata) do
value = extract_measurement(metric, measurements, metadata)
tags = extract_tags(metric, metadata)

metric_name = "#{Enum.join(metric.name, ".")}"
MetricStore.write_metric(name, metric, metric_name, value, tags)
unless is_nil(value) do
tags = extract_tags(metric, metadata)

metric_name = "#{Enum.join(metric.name, ".")}"
MetricStore.write_metric(name, metric, metric_name, value, tags)
end
end
end
rescue
Expand Down
10 changes: 2 additions & 8 deletions lib/otel_metric_exporter/metric_store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ defmodule OtelMetricExporter.MetricStore do
) do
%Metric{
name: Enum.join(name, "."),
description: description,
description: description || "",
unit: convert_unit(unit),
data: convert_data(metric, values)
}
Expand Down Expand Up @@ -337,7 +337,7 @@ defmodule OtelMetricExporter.MetricStore do
}}
end

defp convert_unit(:unit), do: nil
defp convert_unit(:unit), do: ""
defp convert_unit(:second), do: "s"
defp convert_unit(:millisecond), do: "ms"
defp convert_unit(:microsecond), do: "us"
Expand All @@ -349,12 +349,6 @@ defmodule OtelMetricExporter.MetricStore do
defp convert_unit(:terabyte), do: "TBy"
defp convert_unit(x) when is_atom(x), do: Atom.to_string(x)

# These two clauses are here to preserve the current behaviour of the library and avoid
# introducing unexpected errors. Ideally, we would filter these nil values higher up in the
# call stack and stop short of exporting metrics with nil values.
defp convert_value(nil, :int), do: {:as_int, nil}
defp convert_value(nil, :double), do: {:as_double, nil}

@signed_int64_max 2 ** 63 - 1
@signed_int64_min -2 ** 63
defp convert_value(int, _preferred_type)
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading