Skip to content

Implement custom protoc plugin to generate OTLP JSON class definitions#4910

Open
herin049 wants to merge 6 commits intoopen-telemetry:mainfrom
herin049:feat/protoc-otlp-json-plugin
Open

Implement custom protoc plugin to generate OTLP JSON class definitions#4910
herin049 wants to merge 6 commits intoopen-telemetry:mainfrom
herin049:feat/protoc-otlp-json-plugin

Conversation

@herin049
Copy link
Contributor

@herin049 herin049 commented Feb 11, 2026

Description

This PR implements a custom protoc plugin to automatically generate OTLP JSON class definitions. Consensus was reached deciding that a custom protoc plugin would be the best route for adding support for OTLP JSON while minimizing the number of runtime dependencies (for additional context reference #4886). This PR is a refined version of the first part of a set of changes that are in #4902. Please view the draft PR for an overview of how these packages will be used.

Fixes #1003

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

tox run -e $(tox --listenvs | grep opentelemetry-protojson | tr '\n' ',')

Does This PR Require a Contrib Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

@herin049 herin049 requested a review from a team as a code owner February 11, 2026 23:18
@herin049 herin049 force-pushed the feat/protoc-otlp-json-plugin branch from f66380d to a9d10c7 Compare February 12, 2026 04:09
@xrmx xrmx moved this to Ready for review in Python PR digest Feb 12, 2026
@mmanciop
Copy link
Contributor

I am very much looking forward to this landing: protobuf is a pretty toxic dependency when automatically injecting Python SDKs with the OpenTelemetry Injector

@pmcollins
Copy link
Member

I think it would be valuable to add a test that validates the generated JSON by feeding it to google.protobuf.json_format.Parse. Maybe you could then compare dictionary representations of the original object with that of the resulting proto object for equality.

@herin049
Copy link
Contributor Author

I think it would be valuable to add a test that validates the generated JSON by feeding it to google.protobuf.json_format.Parse. Maybe you could then compare dictionary representations of the original object with that of the resulting proto object for equality.

Just to make sure I understand your suggestion properly, currently I have an integration test which runs the plugin to generate the ProtoJSON Python classes and tests the serialization/deserialization. Are you suggesting that we also generate the corresponding Protobuf definitions and then perform the JSON serialization using the ProtoJSON classes, load the JSON using the Protobuf definitions and then compare the attributes on the ProtoJSON and Protobuf classes?

@herin049 herin049 force-pushed the feat/protoc-otlp-json-plugin branch from 4d4119c to 4bce86a Compare February 18, 2026 03:49
@herin049
Copy link
Contributor Author

@pmcollins Since we mostly care about serialization for the SDK, I added tests to use google.protobuf.json_format.MessageToDict and verify that the contents are identical. Let me know what you think, and if there are another other test cases that might be useful to add.

@pmcollins
Copy link
Member

Thanks @herin049. My suggestion is to simulate a Collector receiving JSON. In other words, end-to-end tests that create SDK objects typically sent to an exporter for emission (traces, metrics, and logs), serialize those objects into JSON using the functionality in this PR, send that JSON to our simulated Collector (google.protobuf.json_format.Parse), and make sure that the "Collector" produces data that matches what was fed into the other end of the pipe.

@herin049
Copy link
Contributor Author

@pmcollins I will be adding the opentelemetry-exporter-otlp-json-common and opentelemetry-exporter-otlp-json-http packages in a follow up PR to limit the size of this PR. The opentelemetry-exporter-otlp-json-common will be analogous to the existing opentelemetry-exporter-otlp-proto-common package and will include the logic to convert SDK objects to the ProtoJSON objects in the generated opentelemetry-proto-json package added in this PR. Do we want to add the roundtrip tests you are describing into the opentelemetry-exporter-otlp-proto-common package, and defer this to the next PR? I am open to including them in this PR as well, just not sure where exactly we would want these tests to live. Feel free to look at the draft PR #4902 for additional context.

@pmcollins
Copy link
Member

Makes sense @herin049 -- can be in a subsequent PR.

@herin049 herin049 requested a review from aabmass February 24, 2026 17:29
@herin049 herin049 force-pushed the feat/protoc-otlp-json-plugin branch from 4bce86a to 05b459f Compare February 27, 2026 15:49
@herin049
Copy link
Contributor Author

@aabmass I have rewritten the commit history to separate out the addition of the opentelemetry-codegen-json package an the auto generated opentelemetry-proto-json package with a final commit to integrate these packages into the rest of the repo. Hopefully this makes reviewing a bit easier, let me know if you would prefer that I break down these changes further.

Copy link
Member

@aabmass aabmass left a comment

Choose a reason for hiding this comment

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

It's a bit large LGTM. I will look over the generated code too

def encode_repeated(
values: typing.Optional[list[typing.Any]],
map_fn: typing.Callable[[typing.Any], typing.Any],
) -> list[typing.Any]:
Copy link
Member

Choose a reason for hiding this comment

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

nit use generic

def encode_repeated(
    values: list[T] | None,
    map_fn: typing.Callable[[T], typing.Any],
Comment view) -> list[typing.Any]:


@contextmanager
def code_generation() -> Iterator[
Tuple[plugin.CodeGeneratorRequest, plugin.CodeGeneratorResponse],
Copy link
Member

Choose a reason for hiding this comment

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

nit tuple[...]

Comment on lines +152 to +154
if proto_type in INT64_TYPES:
return True
return proto_type in {
Copy link
Member

Choose a reason for hiding this comment

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

I think this can be a single bool expression return ... or ...

Copy link
Member

Choose a reason for hiding this comment

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

This code LGTM but I would be open to using some off-the-shelf strongly typed lib for this too. Up to you, I'm not sure if something like that exists.

]
)

sys.path.insert(0, str(gen_path.absolute()))
Copy link
Member

Choose a reason for hiding this comment

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



def test_unknown_fields_ignored(
test_v1_types: tuple[Type[Any], Type[Any]],
Copy link
Member

Choose a reason for hiding this comment

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

type[Any]

Copy link
Member

Choose a reason for hiding this comment

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

Oh, does this runtime module need to be available to the generated code as a dependency? I.e. they need to install opentelemetry-codegen-json?

Comment on lines +57 to +64
def to_json(self) -> builtins.str:
"""
Serialize this message to a JSON string.

Returns:
JSON string
"""
return json.dumps(self.to_dict())
Copy link
Member

Choose a reason for hiding this comment

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

Would it be reasonable to put this in a base class to try to reduce the code size a bit? Maybe there are downsides, lmk

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've opted to add a @json_serde class decorator

"""
return json.dumps(self.to_dict())

@builtins.classmethod
Copy link
Member

Choose a reason for hiding this comment

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

Since it's not super obvious to me, what's the need of using builtins? To avoid accidental name clashes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I used this to prevent name clashes. It might be overkill, but I figured it wouldn't hurt. This is also the convention that the mypy protobuf library used which I took inspiration from.

typeCheckingMode = "standard"
pythonVersion = "3.9"

include = [
Copy link
Member

@aabmass aabmass Mar 5, 2026

Choose a reason for hiding this comment

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

Can we typecheck the plugin code here, and ideally even the generated code as a sanity check?

Copy link
Member

@MikeGoldsmith MikeGoldsmith left a comment

Choose a reason for hiding this comment

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

Looks good to me outside of what @aabmass raised. Found one minor typo 👍🏻

Args:
snake_name: The field name in snake_case.
Returns:
The field name converted to camelCase.k
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The field name converted to camelCase.k
The field name converted to camelCase

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

Labels

None yet

Projects

Status: Ready for review

Development

Successfully merging this pull request may close these issues.

Support JSON over HTTP in OTLP exporter

5 participants