Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autoconfiguration wrapper artifact #2401

Merged
merged 22 commits into from
Jan 7, 2021

Conversation

anuraaga
Copy link
Contributor

@anuraaga anuraaga commented Dec 23, 2020

This implements autoconfiguration moved into a separate layer as discussed a few times before. It's aim is to remove autoconfiguration from the core SDK so frameworks with their own configuration system, e.g., sleuth, can use it with full control, and other users can rely on our autoconfiguration if they want. Since we have control of the artifact, while slightly weird, it seems reasonable for us to automatically call into it with reflection, and otherwise we just return no-op (and log in the case of a misconfigured SDK as in #2396).

I mostly followed the parameters supported by the java agent except


private static void configureOtlpMetrics(
ConfigProperties config, SdkMeterProvider meterProvider) {
OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will need to add linkage error guards to return better error messages for when a configuration option is set but implementation not available.

@anuraaga
Copy link
Contributor Author

/cc @marcingrzejszczak you might be interested in this too

@marcingrzejszczak
Copy link
Contributor

@anuraaga thanks for mentioning me! BTW can you start mentioning @jonatan-ivanov too (that's why I suggested @jkwatson to create a Github Team and just add people to the team and mention that team instead of concrete people).

build.gradle Show resolved Hide resolved
@jkwatson
Copy link
Contributor

@anuraaga thanks for mentioning me! BTW can you start mentioning @jonatan-ivanov too (that's why I suggested @jkwatson to create a Github Team and just add people to the team and mention that team instead of concrete people).

It turns out that I don't have permissions to create a team (or at least I get a 404 from github when trying to click the "new team" link). So, @bogdandrutu will have to do this, I think, unless he gives me (and @anuraaga) owner permissions on the repository.

.build();
}

private static void configureOtlpMetrics(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'd break each of these methods out into their own class so it's clear how to add new ones to the auto-config, rather than having to dig into this beast to find them. :)

} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(
"OpenTelemetrySdkAutoConfiguration detected on classpath "
+ "but could not invoke initialize method. This is a bug in OpenTelemetry.",
Copy link
Contributor

Choose a reason for hiding this comment

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

this could also be due to a restrictive security manager

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For a public method, as far as I know there wouldn't be any situation that could be blocked by security manager, is there one?

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought security managers could block all reflection, but I wouldn't swear to it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, a security manager can block all reflection, even for public methods, and even just calls to "getMethod", although I would suspect that's extremely rare these days.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had no idea - do you think I should change the message? Wondering how realistic it is to disable public reflection vs adding noise to this code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just "This could be a bug in OpenTelemetry" ? Really, it doesn't matter all that much.

@jkwatson
Copy link
Contributor

this is pretty slick. It's SPI (albeit a slightly different flavor, with the builder), but it doesn't demand SPI be used. It makes it easy to manually auto-configure or manually configure, or mix-and-match however you like.

👍 to this approach. Obviously, if we want to go this route, we'll want to write some tests for the configuration (or move them over from the existing configuration code/tests), and do some refactor/cleanup so it's not one enormous class, but as a proof-of-concept, this is very nice. 🚀

@codecov
Copy link

codecov bot commented Dec 25, 2020

Codecov Report

Merging #2401 (31853bc) into master (0dac3f6) will increase coverage by 87.85%.
The diff coverage is 41.44%.

Impacted file tree graph

@@              Coverage Diff              @@
##             master    #2401       +/-   ##
=============================================
+ Coverage          0   87.85%   +87.85%     
- Complexity        0     2692     +2692     
=============================================
  Files             0      310      +310     
  Lines             0     9017     +9017     
  Branches          0      964      +964     
=============================================
+ Hits              0     7922     +7922     
- Misses            0      768      +768     
- Partials          0      327      +327     
Impacted Files Coverage Δ Complexity Δ
...sdk/autoconfigure/MetricExporterConfiguration.java 2.04% <2.04%> (ø) 2.00 <2.00> (?)
...try/sdk/autoconfigure/PropagatorConfiguration.java 5.88% <5.88%> (ø) 1.00 <1.00> (?)
...y/sdk/autoconfigure/SpanExporterConfiguration.java 5.88% <5.88%> (ø) 2.00 <2.00> (?)
...toconfigure/OpenTelemetrySdkAutoConfiguration.java 10.71% <10.71%> (ø) 1.00 <1.00> (?)
...java/io/opentelemetry/api/GlobalOpenTelemetry.java 61.11% <27.77%> (ø) 11.00 <2.00> (?)
...opentelemetry/sdk/autoconfigure/ClasspathUtil.java 40.00% <40.00%> (ø) 1.00 <1.00> (?)
...sdk/autoconfigure/TracerProviderConfiguration.java 83.33% <83.33%> (ø) 20.00 <20.00> (?)
...ntelemetry/sdk/autoconfigure/ConfigProperties.java 86.00% <86.00%> (ø) 23.00 <23.00> (?)
...etry/sdk/autoconfigure/ConfigurationException.java 100.00% <100.00%> (ø) 1.00 <1.00> (?)
...elemetry/sdk/testing/junit4/OpenTelemetryRule.java 95.83% <0.00%> (ø) 7.00% <0.00%> (?%)
... and 309 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0dac3f6...06048d4. Read the comment docs.

Copy link
Contributor Author

@anuraaga anuraaga left a comment

Choose a reason for hiding this comment

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

I have a bit more grungework in adding unit tests for ConfigProperties and some failure cases, but I think this is mostly ready for another look

set(autoConfigured);
return autoConfigured;
}

OpenTelemetryFactory openTelemetryFactory = Utils.loadSpi(OpenTelemetryFactory.class);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unlike the first commit with PoC, I haven't removed the API SPI yet, that we would do in a separate PR.

@@ -55,20 +55,6 @@
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #2418

/**
* Auto-configuration for the OpenTelemetry SDK. As an alternative to programmatically configuring
* the SDK using {@link OpenTelemetrySdk#builder()}, this package can be used to automatically
* configure the SDK using environment properties specified by OpenTelemetry.
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'm hoping to link to a docs page from here rather than outlining all properties here - presuably autoconfiguration users are the last to read the javadocs

@jkwatson
Copy link
Contributor

jkwatson commented Jan 4, 2021

This is looking like a fantastic start. @anuraaga What do you think needs to be done to get it out of draft mode? I'd love to move forward with this ASAP, so we can move toward a 1.0 release.

@anuraaga
Copy link
Contributor Author

anuraaga commented Jan 4, 2021

@jkwatson Just a few more boring unit tests I'll knock out today :)

@anuraaga
Copy link
Contributor Author

anuraaga commented Jan 5, 2021

@jkwatson Ok should be good to go

/**
* A service provider interface (SPI) for performing additional programmatic configuration of a
* {@link SdkTracerProviderBuilder} during initialization. When using auto-configuration, you should
* prefer to use system properties for configuration, but this may be useful to register components
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* prefer to use system properties for configuration, but this may be useful to register components
* prefer to use system properties or environment variables for configuration, but this may be useful to register components

return new ConfigProperties((Map) properties, Collections.emptyMap());
}

private final Map<String, String> config;
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be up above the static methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heh - I'm going to have trouble getting used to fields being so far from the constructor :P

Copy link
Member

Choose a reason for hiding this comment

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

that's why static methods should go at the bottom of the class 😁

private final Map<String, String> config;

private ConfigProperties(
Map<Object, Object> systemProperties, Map<String, String> environmentVariables) {
Copy link
Contributor

Choose a reason for hiding this comment

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

the left map could be Map<?,?> then you don't have to cast above.

splitKeyValuePairs -> {
if (splitKeyValuePairs.size() != 2) {
throw new ConfigurationException(
"Map property key missing value: " + name + "=" + config.get(name));
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit. if you have an entry like =f it's technically missing the key, not the value. ;)

Arrays.stream(keyValuePair.split("=", 2))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList()))
Copy link
Contributor

Choose a reason for hiding this comment

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

These 4 lines of code could be extracted to a method like "filterBlanksAndNulls" and re-used above at line 96-99

/** An exception that is thrown if the user-provided configuration is invalid. */
public final class ConfigurationException extends RuntimeException {

private static final long serialVersionUID = 4717640118051490483L;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is necessary or useful. No one should be using default serialization for anything, anywhere.

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 think Java throws a warning which fails our build if I don't have it (agree no one should serialize it :)

warning: [serial] serializable class ConfigurationException has no definition of serialVersionUID

Copy link
Contributor

Choose a reason for hiding this comment

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

that must be an errorprone warning. I can't imagine that javac would emit that.

switch (name) {
case "otlp":
case "otlp_metrics":
if (metricsAlreadyRegistered) {
Copy link
Contributor

Choose a reason for hiding this comment

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

it's a little annoying that this same check has to be done twice, but I don't see any easy away around it without some sort of ugly nesting of switch statements

return true;
default:
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you could do:

    switch (name) {
      case "otlp":
      case "otlp_metrics":
      case "prometheus":
        if (metricsAlreadyRegistered) {
          throw new ConfigurationException(
              "Multiple metrics exporters configured. Only one metrics exporter can be "
                  + "configured at a time.");
        }
        if ("prometheus".equals(name)) {
          configurePrometheusMetrics(config, meterProvider);
          return true;
        }
        configureOtlpMetrics(config, meterProvider);
        return true;
      default:
        return false;
    }

but I don't know if that's really much better.

SdkTracerProvider tracerProvider =
TracerProviderConfiguration.configureTracerProvider(resource, exporterNames, config);

if (!exporterNames.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I really really don't like that the previous code mutates the set and we're relying on that fact here. That was super surprising to me...in fact, I was sure this line was a bug until I dug into the implementations.

Even if it ends up being uglier, I'd very strongly prefer it if we didn't have hidden side-effects in the code like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I mention it in a spec issue too :)

open-telemetry/opentelemetry-specification#1318 (comment)

Do you see an alternative? Should I mutate a handledExportNames with .add instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

You could return an updated copy of the set, maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's tricky since already returning a tracerprovider from one of them. It's a bit of duplication but I went with exposing the recognized names and checking eagerly instead of mutations.

SpanExporter exporter = SpanExporterConfiguration.configureExporter(name, config);
if (exporter != null) {
spanExporters.add(exporter);
exporterNames.remove(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

👎

OpenTelemetrySdkAutoConfiguration.configureResource(
ConfigProperties.createForTest(
Collections.singletonMap("otel.resource.attributes", "telemetry.sdk.name=test")));
assertThat(resource.getAttributes().get(SemanticAttributes.TELEMETRY_SDK_NAME))
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't even realize that this was a thing that end-users could do. Is this desirable behavior?

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 think we'll need it for service name? I don't think a user should do something bad but it seems to make sense semantically to prioritize them if they do go to the effort.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if it's generally desirable or not, but it certainly is the way it works today, so have a test for it is good. :)

void globalOpenTelemetryWhenError() {
Logger logger = Logger.getLogger(GlobalOpenTelemetry.class.getName());
AtomicReference<LogRecord> logged = new AtomicReference<>();
Handler handler =
Copy link
Contributor

Choose a reason for hiding this comment

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

It'll be nice to convert to the logunit code!


Set<String> unrecognizedExporters = new LinkedHashSet<>(exporterNames);
unrecognizedExporters.removeAll(SpanExporterConfiguration.RECOGNIZED_NAMES);
unrecognizedExporters.removeAll(MetricExporterConfiguration.RECOGNIZED_NAMES);
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a good solution. thanks!

Double ratio = config.getDouble("otel.trace.sampler.arg");
if (ratio == null) {
throw new ConfigurationException(
"otel.trace.sampler=traceidratio but otel.trace.sampler.arg is not provided. "
Copy link
Contributor

Choose a reason for hiding this comment

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

stuff like this is a good argument for extracting the keys into constants, but that can be done as a separate step later, for sure.

}

// Visible for testing
@SuppressWarnings({"unchecked", "rawtypes"})
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this supression is needed any more.

Copy link
Contributor

@jkwatson jkwatson left a comment

Choose a reason for hiding this comment

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

Thanks!

Anuraag Agrawal and others added 3 commits January 7, 2021 11:40
@anuraaga anuraaga merged commit e749f2b into open-telemetry:master Jan 7, 2021
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.

4 participants