Skip to content

Conversation

@trask
Copy link
Member

@trask trask commented Dec 15, 2025

@trask trask force-pushed the declarative-config/a-agent-foundation branch 2 times, most recently from 35895af to 92f18c1 Compare December 15, 2025 04:25
@trask trask force-pushed the declarative-config/a-agent-foundation branch from 92f18c1 to 1f144e1 Compare December 15, 2025 04:39
@trask trask changed the title The one true config bridge The one true configuration bridge Dec 15, 2025
@trask trask marked this pull request as ready for review December 15, 2025 05:26
@trask trask requested a review from a team as a code owner December 15, 2025 05:26
Copy link
Member

@zeitlinger zeitlinger left a comment

Choose a reason for hiding this comment

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

Very similar to what I did in #15622 - I focused more on the case where InstrumentationConfig is not available today - so it's mostly complementary (but I didn't check out the big PR yet)


static {
DeclarativeConfigProperties graphqlConfig =
DeclarativeConfigUtil.getStructured(GlobalOpenTelemetry.get(), "java", empty())
Copy link
Member

Choose a reason for hiding this comment

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

"java", empty()) will be there for every call, so I proposed to get rid of it in #15622

List<String> value =
        ConfigProvider.noop()
            .get("http")
            .get("client")
            .getScalarList("known-methods", String.class, Collections.singletonList("GET"));
    assertThat(value).containsExactly("GET");

Copy link
Member

Choose a reason for hiding this comment

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

OK, we do need to be able to access general as well - so maybe just add another getJava for the common case

Copy link
Member Author

Choose a reason for hiding this comment

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

I want to try leaning into using the Declarative Configuration API directly. If it has usability issues let's raise them to @jack-berg.

Copy link
Member

Choose a reason for hiding this comment

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

I don't like that in many cases empty() will be present, but the motivation is that null is different from present and empty.

I'm open to ideas about adding syntactic sugar to facilitate this common task of walking the tree.

Maybe something like:

List<String> value = config.walk("java.http.client", empty()).getScalarList("known-methods", String.class, singletonList("GET"));

Where walk(String dotDelimetedPath, DeclarativeConfigProperties defaultValue) splits the path on ".", and calls getStructured(segment, defaultValue) for each segment.

Or maybe just:

List<String> value = config.walk("java").walk("http").walk("client").getScalarList("known-methods", String.class, singletonList("GET"));

Where walk is an alias for getStructured(String name, empty())

Copy link
Member

Choose a reason for hiding this comment

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

I've created open-telemetry/opentelemetry-java#7923 to address this

@zeitlinger
Copy link
Member

Good to merge from my point of view

static {
DeclarativeConfigProperties graphqlConfig =
DeclarativeConfigUtil.getStructured(GlobalOpenTelemetry.get(), "java", empty())
.getStructured("graphql", empty());
Copy link
Member Author

@trask trask Dec 15, 2025

Choose a reason for hiding this comment

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

need to think about which instrumentation configurations are stable (e.g. as opposed to "graphql/development")

the downside to adding "/development" is that then we have to deal with deprecating the "/development" node later

maybe it's ok to be more aggressive on marking configurations as stable

cc @laurit

Copy link
Member

Choose a reason for hiding this comment

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

We had already discussed that we want to keep accepting the development suffix even after stabilization

Copy link
Member

Choose a reason for hiding this comment

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

An argument for stable is: the whole instrumentation block is already development - but that's more a formal argument

Copy link
Member Author

Choose a reason for hiding this comment

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

We had already discussed that we want to keep accepting the development suffix even after stabilization

yeah, but it's not clear yet if this will be responsibility of each instrumentation to check both, or if this will be handled transparently in the SDK

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe it's ok to be more aggressive on marking configurations as stable

I think this is a good idea. With the current approach most of our instrumentations and their options will be perpetually experimental which isn't really useful.

Comment on lines 25 to 32
Configuration config = new Configuration(openTelemetry);

TELEMETRY =
GraphQLTelemetry.builder(openTelemetry)
.setCaptureQuery(config.captureQuery(true))
.setSanitizeQuery(config.querySanitizerEnabled(true))
.setAddOperationNameToSpanName(config.addOperationNameToSpanName(false))
.build();
Copy link
Member Author

Choose a reason for hiding this comment

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

how do folks like this Configuration class encapsulation as a first stab? the configuration code is kind of ugly (and hard to read) when strewn about

I think we've discussed this kind of thing before where we would ideally generate something like this from a declarative configuration definition somewhat similar to semconv generation

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it's hard to read - I had to check the implementation. Why not assign the default value in the config class?

And yes, it's a great opportunity to do code generation from the metadata yaml file.
I think this could be done in a follow up pr.
@jaydeluca fyi

Copy link
Member Author

Choose a reason for hiding this comment

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

Why not assign the default value in the config class?

oh, true, I forgot that default values are encoded in configuration schema now

though I'm not sure if defaultBehavior is structured enough to use it for code generation

cc @jack-berg

Copy link
Member

Choose a reason for hiding this comment

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

oh, true, I forgot that default values are encoded in configuration schema now

Encoded is a stretch.. We define the behavior, but its up to the implementation to apply that behavior.

And yes, it's a great opportunity to do code generation from the metadata yaml file.

JSON schema does have a default keyword: https://json-schema.org/understanding-json-schema/reference/annotations

It just doesn't make sense to use in declarative config because quite often we have to define defaults which cannot be encoded in a value.

Java instrumentation might be able to use the default annotation, and leverage it in codegen, if it chooses to express the schema for its instrumentations in json schema.

Copy link
Contributor

@laurit laurit Dec 16, 2025

Choose a reason for hiding this comment

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

I don't particularly like it because the original code was simpler. We could consider building our own utilities with simpler api that use the declarative config api under the hood. 99% of time we are reading simple booleans or strings anyway.
We could have something like

class AgentConfiguration {
  private final DeclarativeConfigProperties javaConfig;

  private AgentConfiguration(DeclarativeConfigProperties javaConfig) {
    this.javaConfig = javaConfig;
  }

  // could be a singleton
  static AgentConfiguration get() {
    OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
    DeclarativeConfigProperties javaConfig = empty();
    if (openTelemetry instanceof ExtendedOpenTelemetry) {
      ExtendedOpenTelemetry extendedOpenTelemetry = (ExtendedOpenTelemetry) openTelemetry;
      DeclarativeConfigProperties instrumentationConfig =
          extendedOpenTelemetry.getConfigProvider().getInstrumentationConfig();
      if (instrumentationConfig != null) {
        javaConfig = instrumentationConfig.getStructured("java", empty());
      }
    }

    return new AgentConfiguration(javaConfig);
  }

  boolean getBoolean(boolean defaultValue, String... key) {
    if (key.length == 0) {
      return defaultValue;
    }

    DeclarativeConfigProperties node = javaConfig;
    for (int i = 0; i < key.length - 2; i++) {
      node = node.getStructured(key[i], empty());
    }
    return node.getBoolean(key[key.length - 1], defaultValue);
  }
}

and you could use it with

AgentConfiguration.get().getBoolean(true, "graphql", "query_sanitizer", "enabled");

could also add

    boolean getBoolean(String key, boolean defaultValue) {
      return getBoolean(defaultValue, key.split("\\."));
    }

then you could use it with

AgentConfiguration.get().getBoolean("graphql.query_sanitizer.enabled", true);

which would be pretty much the same as the exiting code

Copy link
Member Author

Choose a reason for hiding this comment

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

This part:

    OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
    DeclarativeConfigProperties javaConfig = empty();
    if (openTelemetry instanceof ExtendedOpenTelemetry) {
      ExtendedOpenTelemetry extendedOpenTelemetry = (ExtendedOpenTelemetry) openTelemetry;
      DeclarativeConfigProperties instrumentationConfig =
          extendedOpenTelemetry.getConfigProvider().getInstrumentationConfig();
      if (instrumentationConfig != null) {
        javaConfig = instrumentationConfig.getStructured("java", empty());
      }
    }

should soon be simplified to:

GlobalOpenTelemetry.get().getConfigProvider().getInstrumentationConfig().getStructured("java", empty())

(with stabilization of getConfigProvider and open-telemetry/opentelemetry-java#7920)

if the other part is still too complex for our preference, I'd like to try to improve the Declarative Configuration API ergonomics (instead of adding our own wrapper)

e.g. @zeitlinger has a proposal along these lines: open-telemetry/opentelemetry-java#7923

are there other things from your perspective that would make using the Declarative Configuration API directly better for us?

Copy link
Member

@jack-berg jack-berg Dec 16, 2025

Choose a reason for hiding this comment

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

AgentConfiguration.get().getBoolean("graphql.query_sanitizer.enabled", true);

With open-telemetry/opentelemetry-java#7920 and open-telemetry/opentelemetry-java#7923 (and eventual stabilization of these APIs) we'll end up with something similar:

GlobalOpenTelemetry.getConfigProvider().getInstrumentationConfig()
  .get("graphql)
  .get("query_sanitizer")
  .getBoolean("enabled", true);

And the agent could provide instrumentation helpers for the repetitive GlobalOpenTelemetry.getConfigProvider().getInstrumentationConfig() part.

Copy link
Member

Choose a reason for hiding this comment

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

Actually it would be

GlobalOpenTelemetry.get().getConfigProvider().getInstrumentationConfig()
  .get("java")
  .get("graphql")
  .get("query_sanitizer")
  .getBoolean("enabled", true);

@jack-berg difference to what you wrote

  • GlobalOpenTelemetry.getConfigProvider() doesn't exist yet (but I think you implied that it would be added)
  • .get("java") was missing

This inspired me to create open-telemetry/opentelemetry-java#7927 so it can become

GlobalOpenTelemetry.get().getConfigProvider()
  .get("graphql")
  .get("query_sanitizer")
  .getBoolean("enabled", true);
  • removes .getInstrumentationConfig()
  • removes `.get("java")

@trask trask force-pushed the declarative-config/a-agent-foundation branch from 3793fc2 to 4dca57c Compare December 15, 2025 20:29
@trask trask force-pushed the declarative-config/a-agent-foundation branch from 4dca57c to 3baf749 Compare December 15, 2025 20:30
@trask
Copy link
Member Author

trask commented Dec 16, 2025

@laurit I'm going to go ahead and merge so I can submit a couple of follow-ups, but feel free to leave comments here and I'll follow-up on them

@trask trask merged commit 92475f1 into open-telemetry:main Dec 16, 2025
85 checks passed
@trask trask deleted the declarative-config/a-agent-foundation branch December 16, 2025 00:54
return String.join(".", path) + "." + name;
}

private static String toPropertyKey(String fullPath) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to add some way to customize this to make it usable in vendor distros that could need to map their of configuration properties?

Copy link
Member

Choose a reason for hiding this comment

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

In my current PoC, I have a mapper class that some common default properties do declarative config, based on what was specified as system properties:

https://github.com/grafana/grafana-opentelemetry-java/blob/c0e5e7f7093fbd341eee29eb9f2342d5a6d1d9b1/custom/src/main/java/com/grafana/extensions/DeclarativeConfigPropertyCustomizer.java#L13

It leaves enough wiggle room for vendor conventions.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

4 participants