Skip to content

[EXPORTER] Add a JsonWriter interface for OTLP/JSON serialization - #4556

Open
ltowarek wants to merge 18 commits into
open-telemetry:mainfrom
ltowarek:json-writer-seam
Open

ltowarek wants to merge 18 commits into
open-telemetry:mainfrom
ltowarek:json-writer-seam

Conversation

@ltowarek

Copy link
Copy Markdown
Contributor

The OTLP HTTP and file clients each carried their own copy of the same reflection-based protobuf-to-JSON converter. Replace both with a single converter that emits through a JsonWriter interface, and let callers supply their own writer through a JsonWriterFactory, as they already can for the HTTP client. The emitted JSON is unchanged.

Fixes #2541

Changes

The factory is injected as a json_writer_factory field on the existing
runtime options, rather than as new constructor and factory overloads - those
would have added roughly 50 parallel entry points across the exporters for one
injectable. No existing signature changes; a null factory selects the default
nlohmann backend.

OTELCPP_WITH_JSON_WRITER_NLOHMANN controls whether that backend is compiled.
OTELCPP_WITH_OTLP_HTTP and OTELCPP_WITH_OTLP_FILE no longer force
nlohmann-json on, so a consumer supplying their own writer can drop it from the
build entirely.

Golden-body tests pin the complete request body for traces, metrics and logs on
both exporters, so the unchanged-output claim rests on the emitted bytes.

For significant contributions please make sure you have completed the following items:

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

The OTLP HTTP and file clients each carried their own copy of the same
reflection-based protobuf-to-JSON converter. Replace both with a single
converter that emits through a JsonWriter interface, and let callers supply
their own writer through a JsonWriterFactory. The emitted JSON is unchanged.

The factory is injected the way each family already injects
thread_instrumentation: as a field on OtlpHttpClientOptions for the HTTP
exporters, and on OtlpFileClientRuntimeOptions for the file ones. No existing
constructor or factory signature changes, and a null factory selects the
default backend.
@ltowarek
ltowarek requested a review from a team as a code owner September 10, 2026 21:39
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.55297% with 83 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.38%. Comparing base (eb92442) to head (8ea7620).

Files with missing lines Patch % Lines
exporters/otlp/src/otlp_json_converter.cc 68.79% 54 Missing ⚠️
exporters/otlp/src/otlp_json_writer_nlohmann.cc 72.90% 29 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4556      +/-   ##
==========================================
- Coverage   86.50%   86.38%   -0.12%     
==========================================
  Files         525      530       +5     
  Lines       20465    20135     -330     
==========================================
- Hits        17701    17391     -310     
+ Misses       2764     2744      -20     
Files with missing lines Coverage Δ
...xporters/otlp/detail/default_json_writer_factory.h 100.00% <100.00%> (ø)
...de/opentelemetry/exporters/otlp/otlp_file_client.h 100.00% <ø> (ø)
.../exporters/otlp/otlp_file_client_runtime_options.h 100.00% <ø> (ø)
...de/opentelemetry/exporters/otlp/otlp_http_client.h 100.00% <ø> (ø)
...xporters/otlp/otlp_http_exporter_runtime_options.h 100.00% <ø> (ø)
...lp/otlp_http_log_record_exporter_runtime_options.h 100.00% <ø> (ø)
...s/otlp/otlp_http_metric_exporter_runtime_options.h 100.00% <ø> (ø)
...de/opentelemetry/exporters/otlp/otlp_json_writer.h 100.00% <100.00%> (ø)
...elemetry/exporters/otlp/otlp_json_writer_factory.h 100.00% <100.00%> (ø)
exporters/otlp/src/otlp_file_client.cc 72.84% <100.00%> (+2.13%) ⬆️
... and 6 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ltowarek added a commit to ltowarek/esp-opentelemetry-cpp that referenced this pull request Sep 11, 2026
…ction

Advances third_party/opentelemetry-cpp to ltowarek/opentelemetry-cpp@2fb8632c,
otlp-json-no-protobuf rebased onto the current json-writer-seam
(open-telemetry/opentelemetry-cpp#4556). The previous pin, 4a8e7bf4, is no
longer on any fork branch.

PR1 now takes the JsonWriterFactory as a json_writer_factory field on the
exporters' runtime options instead of constructor overloads, so the JTAG file
exporters and the protobuf-encoding OTLP/HTTP exporters set it there through
CjsonRuntimeOptions<T>(). The OTLP/JSON exporters still take both factories as
constructor arguments and are unchanged.

Not yet built with ESP-IDF.

Part of #102

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FvAjrPuAUXJLv8ZpUxh96
Comment thread exporters/otlp/src/otlp_http_client.cc Outdated

std::string post_body_json =
json_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
std::string post_body_json = json_writer->ToString();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we check ok() after ToString() too? That call can fail, and we currently continue with an empty body. The file client needs the same check. Also better to have a test with a writer that fails during ToString() for both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1a9780e: both clients now fail the export when ok() is false after ToString() or when Create() returns nullptr, with tests for each case in both clients.

Comment thread exporters/otlp/src/otlp_http_client.cc Outdated
: is_shutdown_(false),
options_(std::move(options)),
http_client_(std::move(http_client)),
json_writer_factory_(options_.json_writer_factory ? options_.json_writer_factory

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we resolve the default JSON writer only when the content type is JSON? With nlohmann disabled and no custom writer, this currently calls std::terminate() even for binary HTTP export. Please also cover binary export without a JSON backend in a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fc0f530: the default factory is resolved only for kJson content, and BinaryExportDoesNotNeedAJsonWriterFactory covers binary export.

Comment thread exporters/otlp/CMakeLists.txt Outdated
Comment on lines +187 to +191
opentelemetry_add_pkgconfig(
exporter_otlp_json_converter
"OpenTelemetry OTLP - JSON Converter"
"Shared reflection-based protobuf-to-JSON converter for OTLP/JSON exporters."
"opentelemetry_sdk opentelemetry_common opentelemetry_exporter_otlp_json_writer"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we update the full pkg-config dependency chain for these new libraries? The HTTP/file client packages do not include the converter, the writer package does not include the enabled nlohmann backend, and opentelemetry_sdk has no .pc file. An installed static consumer should be able to link using pkg-config with the backend enabled or disabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 803fcce: the client packages now require the writer and converter, the writer requires the nlohmann backend when enabled, the converter requires opentelemetry_proto instead of opentelemetry_sdk, and ci/verify_packages.sh checks the new packages.

if (nlohmann::json *slot = ClaimSlot())
{
*slot = opentelemetry::sdk::common::Base64Escape(
std::string(reinterpret_cast<const char *>(data), size));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pass a nostd::string_view directly to Base64Escape? It already accepts one, so creating a string here adds a full copy of every bytes value before encoding it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 30f1738, thanks.

Fail("Key() called twice without an intervening value");
return;
}
pending_key_ = std::string(key.data(), key.size());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use pending_key_.assign(key.data(), key.size()) here? That lets us reuse the existing capacity instead of constructing another string for every key. Longer field names can otherwise allocate on each call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 7eec588, then replaced in b9530db by inserting the member in Key() and writing the value in place, which removes the key buffer entirely.

virtual void BeginArray() noexcept = 0;
virtual void EndArray() noexcept = 0;

virtual void Key(nostd::string_view key) noexcept = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we document the expectations for custom writers here? In particular, string views must be consumed or copied before the call returns, WriteBytes produces base64, and callers need to know how ok() and ToString() behave after a failure.

The factory should also say whether Create() can return null or throw, and whether calls may happen concurrently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6a869fc, with the whole documented contract covered by a reusable test suite in 5c4e118.

@lalitb

lalitb commented Sep 12, 2026

Copy link
Copy Markdown
Member

Thanks for the PR. The approach looks find at initial glance. Could we add a benchmark comparing JSON serialization before and after this change? It would help check whether the new writer adds any overhead for users keeping the default nlohmann backend. Ideally, measure serialization without HTTP or file I/O.

@ltowarek

Copy link
Copy Markdown
Contributor Author

Thanks for the PR. The approach looks find at initial glance. Could we add a benchmark comparing JSON serialization before and after this change? It would help check whether the new writer adds any overhead for users keeping the default nlohmann backend. Ideally, measure serialization without HTTP or file I/O.

Added in f35605b (serialization only, no I/O); after the optimizations in b9530db, 94b2f64 and ff92b5b, the PR is on par with main for 1 span and faster for larger batches (median of 8 interleaved runs, -O3, i5-4590):

Spans main (1143e55) this PR Difference
1 14.74 µs 14.76 µs +0.1%
10 112.78 µs 109.94 µs −2.5%
100 1,309.96 µs 1,286.79 µs −1.8%
1000 15,074.13 µs 14,592.15 µs −3.2%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSON Serialization Performance

2 participants