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

Create OtelVersion class at build time. #5365

Merged
merged 10 commits into from
Apr 15, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.opentelemetry.gradle

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.the
import java.io.File

/**
* This gradle plugin will define a new task called generateOtelVersionClass.
* This task generates a Java source file that contains the project version
* as a string constant. The "compileJava" task is updated to depend on
* generateOtelVersionClass, and the project source set is updated to
* include the new file.
*/
class OtelVersionClassPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply(JavaPlugin::class.java)

project.task("generateOtelVersionClass") {
doLast {
writeFile(project)
}
}
addTaskDependency(project)
addSourceDependency(project)
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
}

private fun writeFile(project: Project) {
val group = "${project.group}".replace('.', '/')
val projectName = project.name.replace('-', '/')
val outDir = buildOutDir(project)
val filename = "$group/$projectName/internal/OtelVersion.java"
val outFile = File(outDir, filename)
val packageName = "${project.group}.${project.name.replace('-', '.')}.internal"
val classBody = getClassBody("${project.version}", packageName)

outFile.parentFile.mkdirs()
outFile.writeText(classBody)
}

private fun getClassBody(version: String, packageName: String): String {
return """
package $packageName;

/**
* Autogenerated class do not edit.
*/
public final class OtelVersion {
Copy link
Contributor

Choose a reason for hiding this comment

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

My only concern with this approach: will devs have to run a gradle build in order to be able to compile in IDEA? Or is IDEA smart enough to run what needs to be run and get this file into the generated source when the project is imported?

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'm sure a gradle build step has to happen before Resource can compile. Isn't that the same with autovalue tho?

Another option was to do string substitution on the actual source tree, but that leaves uncommitted changes. Open to alternatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is one idea of how to compromise: https://gist.github.com/breedx-splk/01278cc3eb94415353cac1f9cdce89c7

The idea is instead of referencing a static field constant, try to do the same with reflection, and if that fails, fall back to the previous behavior. I don't love it.

public static final String VERSION = "$version";
private OtelVersion(){}
}
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
""".trimIndent()
}

private fun addTaskDependency(project: Project) {
project.tasks.getByName("compileJava") {
dependsOn("generateOtelVersionClass")
}
}

private fun addSourceDependency(project: Project) {
val outDir = buildOutDir(project)
val java = project.the<JavaPluginExtension>()
java.sourceSets.getByName("main").java {
srcDir(outDir)
}
}

private fun buildOutDir(project: Project): File {
return File(project.buildDir, "generated-src/version/java")
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class ProtoFieldsWireHandler : SchemaHandler() {

for (field in type.fieldsAndOneOfFields) {
builder.addField(
FieldSpec.builder(PROTO_FIELD_INFO, field.name.toUpperCase(), PUBLIC, STATIC, FINAL)
FieldSpec.builder(PROTO_FIELD_INFO, field.name.uppercase(), PUBLIC, STATIC, FINAL)
.initializer("\$T.create(\$L, \$L, \"\$L\")",
PROTO_FIELD_INFO,
field.tag,
Expand Down
4 changes: 3 additions & 1 deletion sdk/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import io.opentelemetry.gradle.OtelVersionClassPlugin

plugins {
id("otel.java-conventions")
id("otel.publish-conventions")

id("otel.animalsniffer-conventions")
}
apply<OtelVersionClassPlugin>()
Copy link
Member

Choose a reason for hiding this comment

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

If we do this for :sdk:common, should we do it everywhere?

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 only wanted to do this in the one place where it's used. If there are other places that ever need to reference the class, then those modules can apply the plugin. I don't think we need it floating around every module. Feels bloaty.

Copy link
Member

Choose a reason for hiding this comment

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

That seems reasonable. Here are some other places we read the version:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And those will still continue work, I didn't remove the properties behavior. Are you suggesting that we should tackle those all in this same PR?

Copy link
Member

Choose a reason for hiding this comment

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

Are you suggesting that we should tackle those all in this same PR?

If we have this pattern, it offers a pretty clean way of accessing the version. Adding a plugin to the gradle build is cleaner than copy / pasting the code to read from the resource.

Doesn't have to be in this PR, but if we like this pattern I think we should standardize on it.


description = "OpenTelemetry SDK Common"
otelJava.moduleName.set("io.opentelemetry.sdk.common")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.internal.StringUtils;
import io.opentelemetry.api.internal.Utils;
import io.opentelemetry.sdk.common.internal.OtelVersion;
import java.util.Objects;
import java.util.Properties;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
Expand Down Expand Up @@ -54,7 +54,7 @@ public abstract class Resource {
Attributes.builder()
.put(TELEMETRY_SDK_NAME, "opentelemetry")
.put(TELEMETRY_SDK_LANGUAGE, "java")
.put(TELEMETRY_SDK_VERSION, readVersion())
.put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION)
.build());
}

Expand Down Expand Up @@ -109,18 +109,6 @@ public static Resource create(Attributes attributes, @Nullable String schemaUrl)
return new AutoValue_Resource(schemaUrl, attributes);
}

private static String readVersion() {
Properties properties = new Properties();
try {
properties.load(
Resource.class.getResourceAsStream("/io/opentelemetry/sdk/common/version.properties"));
} catch (Exception e) {
// we left the attribute empty
return "unknown";
}
return properties.getProperty("sdk.version", "unknown");
}

/**
* Returns the URL of the OpenTelemetry schema used by this resource. May be null.
*
Expand Down