diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/WeaverTasks.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/WeaverTasks.kt new file mode 100644 index 000000000..3cdf8c4fe --- /dev/null +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/WeaverTasks.kt @@ -0,0 +1,134 @@ +package io.opentelemetry.gradle + +import java.io.IOException +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.gradle.work.DisableCachingByDefault +import javax.inject.Inject + +@DisableCachingByDefault(because = "Docker run is external and side-effectful") +abstract class WeaverTasks @Inject constructor( + private val execOps: ExecOperations +) : DefaultTask() { + + companion object { + private const val WEAVER_MODEL_PATH = "/home/weaver/model" + private const val WEAVER_TEMPLATES_PATH = "/home/weaver/templates" + private const val WEAVER_TARGET_PATH = "/home/weaver/target" + } + + @get:Input + abstract val dockerExecutable: Property + @get:Input + abstract val platform: Property + @get:Input + abstract val image: Property + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val modelDir: DirectoryProperty + + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val templatesDir: DirectoryProperty + + // Choose ONE of these per task + @get:OutputDirectory + @get:Optional + abstract val outputDir: DirectoryProperty + @get:OutputFile + @get:Optional + abstract val outputFile: RegularFileProperty + + // e.g., ["registry","check","--registry=/home/weaver/model"] + @get:Input + abstract val toolArgs: ListProperty + + @TaskAction + fun runWeaver() { + validateDockerAvailable() + + val mounts = mutableListOf( + "--mount", "type=bind,source=${modelDir.get().asFile.absolutePath},target=$WEAVER_MODEL_PATH,readonly" + ) + + val templates = templatesDir.orNull + if (templates != null) { + when { + templates.asFile.isDirectory -> { + mounts += listOf("--mount", "type=bind,source=${templates.asFile.absolutePath},target=$WEAVER_TEMPLATES_PATH,readonly") + } + templates.asFile.exists() -> { + logger.warn("templatesDir exists but is not a directory: ${templates.asFile.absolutePath}. Skipping templates mount.") + } + } + } + + val targetMount = when { + outputDir.isPresent -> { + outputDir.get().asFile.mkdirs() + listOf("--mount", "type=bind,source=${outputDir.get().asFile.absolutePath},target=$WEAVER_TARGET_PATH") + } + + outputFile.isPresent -> { + // Mount parent directory and ensure weaver writes to the correct filename + val outputFileObj = outputFile.get().asFile + val parent = outputFileObj.parentFile.also { it.mkdirs() } + logger.info("Mounting ${parent.absolutePath} for output file: ${outputFileObj.name}") + listOf("--mount", "type=bind,source=${parent.absolutePath},target=$WEAVER_TARGET_PATH") + } + + else -> error("Either outputDir or outputFile must be set") + } + mounts += targetMount + + val base = mutableListOf("run", "--rm", "--platform=${platform.get()}") + val os = System.getProperty("os.name").lowercase() + if (os.contains("linux")) { + try { + val uid = ProcessBuilder("id", "-u").start().inputStream.bufferedReader().readText().trim() + val gid = ProcessBuilder("id", "-g").start().inputStream.bufferedReader().readText().trim() + base += listOf("-u", "$uid:$gid") + } catch (e: IOException) { + logger.warn("Could not determine uid/gid: ${e.message}. Generated files may be owned by root") + } + } + + execOps.exec { + executable = dockerExecutable.get() + args = base + mounts + listOf(image.get()) + toolArgs.get() + standardOutput = System.out + errorOutput = System.err + isIgnoreExitValue = false + } + } + + private fun validateDockerAvailable() { + try { + val process = ProcessBuilder(dockerExecutable.get(), "--version") + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + if (exitCode != 0) { + throw GradleException("Docker is not available or not functioning correctly. Please ensure Docker is installed and running.") + } + } catch (e: IOException) { + throw GradleException("Docker is required but could not be executed. Please install and start Docker. Error: ${e.message}", e) + } + } +} + diff --git a/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts index dec171500..81ac36af4 100644 --- a/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts @@ -58,7 +58,7 @@ publishing { connection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-contrib.git") developerConnection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-contrib.git") tag.set(tagVersion) - url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/${tagVersion}") + url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/$tagVersion") } issueManagement { diff --git a/buildSrc/src/main/kotlin/otel.weaver-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.weaver-conventions.gradle.kts new file mode 100644 index 000000000..eed3fc949 --- /dev/null +++ b/buildSrc/src/main/kotlin/otel.weaver-conventions.gradle.kts @@ -0,0 +1,188 @@ +import io.opentelemetry.gradle.WeaverTasks +import org.gradle.api.GradleException +import org.gradle.api.provider.Property + +// Weaver code generation convention plugin for OpenTelemetry +// Apply this plugin to modules that have a model/ directory with weaver model files +// It will generate Java code, documentation, and YAML configs using the OpenTelemetry Weaver tool + +val weaverContainer = + "otel/weaver:v0.18.0@sha256:5425ade81dc22ddd840902b0638b4b6a9186fb654c5b50c1d1ccd31299437390" + +// Auto-detect platform for Docker, with fallback to x86_64 +val dockerPlatform = System.getProperty("os.arch").let { arch -> + when { + arch == "aarch64" || arch == "arm64" -> "linux/arm64" + else -> "linux/x86_64" + } +} + +interface OtelWeaverExtension { + /** + * REQUIRED: The Java package path where generated code will be placed. Path should use forward + * slashes (e.g., "io/opentelemetry/ibm/mq/metrics"). + * + * Example configuration in build.gradle.kts: + * ```kotlin + * otelWeaver { + * javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics") + * } + * ``` + */ + val javaOutputPackage: Property +} + +val weaverExtension = extensions.create("otelWeaver", OtelWeaverExtension::class.java) + +val projectModelDir = layout.projectDirectory.dir("model") +val hasWeaverModel = projectModelDir.asFile.exists() && projectModelDir.asFile.isDirectory + +if (hasWeaverModel) { + val projectTemplatesDir = layout.projectDirectory.dir("templates") + val projectDocsDir = layout.projectDirectory.dir("docs") + + logger.lifecycle("Weaver model found in ${project.name}") + logger.lifecycle(" Model directory: ${projectModelDir.asFile.absolutePath}") + logger.lifecycle(" Templates directory: ${projectTemplatesDir.asFile.absolutePath}") + logger.lifecycle(" Container: $weaverContainer") + + tasks.register("weaverCheck") { + group = "weaver" + description = "Check the weaver model for errors" + + dockerExecutable.set("docker") + platform.set(dockerPlatform) + image.set(weaverContainer) + + modelDir.set(projectModelDir) + templatesDir.set(projectTemplatesDir) + outputDir.set(layout.buildDirectory.dir("weaver-check")) + + toolArgs.set(listOf("registry", "check", "--registry=/home/weaver/model")) + + // Always run check task to ensure model validity, even if inputs haven't changed. + // This is intentional as validation should always run when explicitly requested. + outputs.upToDateWhen { false } + } + + tasks.register("weaverGenerateDocs") { + group = "weaver" + description = "Generate markdown documentation from weaver model" + + dockerExecutable.set("docker") + platform.set(dockerPlatform) + image.set(weaverContainer) + + modelDir.set(projectModelDir) + templatesDir.set(projectTemplatesDir) + outputDir.set(projectDocsDir) + + toolArgs.set( + listOf( + "registry", + "generate", + "--registry=/home/weaver/model", + "markdown", + "--future", + "/home/weaver/target" + ) + ) + } + + val weaverGenerateJavaTask = + tasks.register("weaverGenerateJava") { + group = "weaver" + description = "Generate Java code from weaver model" + + dockerExecutable.set("docker") + platform.set(dockerPlatform) + image.set(weaverContainer) + + modelDir.set(projectModelDir) + templatesDir.set(projectTemplatesDir) + + // Map the javaOutputPackage to the output directory + // Finalize the value to ensure it's set at configuration time and avoid capturing the extension + val javaPackage = weaverExtension.javaOutputPackage + javaPackage.finalizeValueOnRead() + outputDir.set(javaPackage.map { layout.projectDirectory.dir("src/main/java/$it") }) + + toolArgs.set( + listOf( + "registry", + "generate", + "--registry=/home/weaver/model", + "java", + "--future", + "/home/weaver/target" + ) + ) + + doFirst { logger.lifecycle(" Java output: ${outputDir.get().asFile.absolutePath}") } + } + + // Validate the required configuration at configuration time (not execution time) + afterEvaluate { + if (weaverExtension.javaOutputPackage.orNull == null) { + throw GradleException( + """ + otelWeaver.javaOutputPackage must be configured in project '${project.name}'. + + Add this to your build.gradle.kts: + otelWeaver { + javaOutputPackage.set("io/opentelemetry/your/package") + } + """.trimIndent() + ) + } + } + + // Make spotless tasks always run after the generate task + tasks + .matching { + it.name == "spotlessJava" || it.name == "spotlessJavaApply" || it.name == "spotlessApply" + } + .configureEach { mustRunAfter(weaverGenerateJavaTask) } + + // Make weaverGenerateJava automatically format generated code + weaverGenerateJavaTask.configure { finalizedBy("spotlessJavaApply") } + + tasks.register("weaverGenerateYaml") { + group = "weaver" + description = "Generate YAML configuration from weaver model" + + dockerExecutable.set("docker") + platform.set(dockerPlatform) + image.set(weaverContainer) + + modelDir.set(projectModelDir) + templatesDir.set(projectTemplatesDir) + outputFile.set(layout.projectDirectory.file("config.yml")) + + toolArgs.set( + listOf( + "registry", + "generate", + "--registry=/home/weaver/model", + "yaml", + "--future", + "/home/weaver/target" + ) + ) + } + + tasks.register("weaverGenerate") { + description = "Generate all outputs (Java, docs, YAML) from weaver model" + group = "weaver" + dependsOn("weaverGenerateJava", "weaverGenerateDocs", "weaverGenerateYaml") + } + + // Ensure proper task ordering without forcing automatic execution + // Use mustRunAfter so weaver generation only runs when explicitly invoked + tasks.named("compileJava") { mustRunAfter(weaverGenerateJavaTask) } + tasks.named("sourcesJar") { mustRunAfter(weaverGenerateJavaTask) } +} else { + logger.debug( + "No weaver model directory found in ${project.name}, skipping weaver task registration" + ) +} diff --git a/docs/weaver.md b/docs/weaver.md new file mode 100644 index 000000000..c823b35a1 --- /dev/null +++ b/docs/weaver.md @@ -0,0 +1,138 @@ +# Weaver Code Generation + +This project uses [OpenTelemetry Weaver](https://github.com/open-telemetry/weaver) to generate code, documentation, and configuration +from semantic convention models. As of now, this is only used for metrics in select modules, but use +cases are likely to expand in the future. + +## Overview + +[Weaver](https://github.com/open-telemetry/weaver) is a tool that generates consistent code across +OpenTelemetry implementations by processing semantic convention models defined in YAML format. +The `otel.weaver-conventions` Gradle plugin automates this process. + +## Using the Weaver Plugin + +### Prerequisites + +- **Docker** must be installed and running + +### Applying to Your Module + +Add the plugin to your module's `build.gradle.kts`: + +```kotlin +plugins { + id("otel.weaver-conventions") +} +``` + +The plugin automatically detects modules with a `model/` directory and registers code generation +tasks. + +### Configuring Java Output Package + +**REQUIRED:** You must explicitly configure the Java output package path: + +```kotlin +otelWeaver { + javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics") +} +``` + +This determines where generated Java code will be placed under `src/main/java/`. + +**Important:** + +- Use forward slashes (`/`) for the path, not dots or backslashes +- The path should match your module's package structure +- Generated code will be placed in `src/main/java/{your-path}/` + +**Example:** + +```kotlin +// For module "io.opentelemetry.contrib.ibm-mq-metrics" +otelWeaver { + javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics") +} +// Generates to: src/main/java/io/opentelemetry/ibm/mq/metrics/ +``` + +### Module Structure + +``` +your-module/ +├── build.gradle.kts +├── model/ +│ ├── registry_manifest.yaml # Weaver registry manifest +│ └── metrics.yaml # Your semantic conventions +├── templates/ # (Optional) Custom code generation templates +├── docs/ +│ └── metrics.md # Generated documentation +``` + +## Available Tasks + +### Generate All Artifacts + +```bash +./gradlew :your-module:weaverGenerate +``` + +Generates Java code, markdown documentation, and YAML configuration. + +### Generate Java Code Only + +```bash +./gradlew :your-module:weaverGenerateJava +``` + +- Outputs to `src/main/java/{configured-package}/` +- **Automatically formats** generated code with `spotlessJavaApply` + +### Generate Documentation + +```bash +./gradlew :your-module:weaverGenerateDocs +``` + +Generates markdown documentation to `docs/metrics.md`. + +### Generate Configuration + +```bash +./gradlew :your-module:weaverGenerateYaml +``` + +Generates a YAML configuration template to `config.yml` in the module root. + +### Validate Model + +```bash +./gradlew :your-module:weaverCheck +``` + +Validates the weaver model for errors without generating code. + +## Example + +The `ibm-mq-metrics` module demonstrates weaver usage: + +```kotlin +// ibm-mq-metrics/build.gradle.kts +plugins { + id("otel.weaver-conventions") +} + +otelJava.moduleName.set("io.opentelemetry.contrib.ibm-mq-metrics") + +otelWeaver { + javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics") +} +// Generates to: src/main/java/io/opentelemetry/ibm/mq/metrics/ +``` + +## Resources + +- [OpenTelemetry Weaver Documentation](https://github.com/open-telemetry/weaver) +- [Semantic Conventions](https://github.com/open-telemetry/semantic-conventions) +- [IBM MQ Metrics Example](../ibm-mq-metrics/) diff --git a/ibm-mq-metrics/Makefile b/ibm-mq-metrics/Makefile deleted file mode 100644 index d6ad8e42a..000000000 --- a/ibm-mq-metrics/Makefile +++ /dev/null @@ -1,84 +0,0 @@ -# From where to resolve the containers (e.g. "otel/weaver"). -WEAVER_CONTAINER_REPOSITORY=docker.io -# Versioned, non-qualified references to containers used in this Makefile. -# These are parsed from dependencies.Dockerfile so dependabot will autoupdate -# the versions of docker files we use. -VERSIONED_WEAVER_CONTAINER_NO_REPO=$(shell cat weaver.Dockerfile | awk '$$4=="weaver" {print $$2}') -# Versioned, non-qualified references to containers used in this Makefile. -WEAVER_CONTAINER=$(WEAVER_CONTAINER_REPOSITORY)/$(VERSIONED_WEAVER_CONTAINER_NO_REPO) - -# Next - we want to run docker as our local file user, so generated code is not -# owned by root, and we don't give unnecessary access. -# -# Determine if "docker" is actually podman -DOCKER_VERSION_OUTPUT := $(shell docker --version 2>&1) -DOCKER_IS_PODMAN := $(shell echo $(DOCKER_VERSION_OUTPUT) | grep -c podman) -ifeq ($(DOCKER_IS_PODMAN),0) - DOCKER_COMMAND := docker -else - DOCKER_COMMAND := podman -endif -DOCKER_RUN=$(DOCKER_COMMAND) run -DOCKER_USER=$(shell id -u):$(shell id -g) -DOCKER_USER_IS_HOST_USER_ARG=-u $(DOCKER_USER) -ifeq ($(DOCKER_COMMAND),podman) - # On podman, additional arguments are needed to make "-u" work - # correctly with the host user ID and host group ID. - # - # Error: OCI runtime error: crun: setgroups: Invalid argument - DOCKER_USER_IS_HOST_USER_ARG=--userns=keep-id -u $(DOCKER_USER) -endif - -.PHONY: generate-docs -generate-docs: - mkdir -p docs - $(DOCKER_RUN) --rm \ - $(DOCKER_USER_IS_HOST_USER_ARG) \ - --mount 'type=bind,source=$(PWD)/model,target=/home/weaver/model,readonly' \ - --mount 'type=bind,source=$(PWD)/templates,target=/home/weaver/templates,readonly' \ - --mount 'type=bind,source=$(PWD)/docs,target=/home/weaver/target' \ - ${WEAVER_CONTAINER} registry generate \ - --registry=/home/weaver/model \ - markdown \ - --future \ - /home/weaver/target - -.PHONY: check -check: - $(DOCKER_RUN) --rm \ - $(DOCKER_USER_IS_HOST_USER_ARG) \ - --mount 'type=bind,source=$(PWD)/model,target=/home/weaver/model,readonly' \ - --mount 'type=bind,source=$(PWD)/templates,target=/home/weaver/templates,readonly' \ - --mount 'type=bind,source=$(PWD)/docs,target=/home/weaver/target' \ - ${WEAVER_CONTAINER} registry check \ - --registry=/home/weaver/model - -.PHONY: generate-java -generate-java: - mkdir -p src/main/java/io/opentelemetry/ibm/mq/metrics - $(DOCKER_RUN) --rm \ - $(DOCKER_USER_IS_HOST_USER_ARG) \ - --mount 'type=bind,source=$(PWD)/model,target=/home/weaver/model,readonly' \ - --mount 'type=bind,source=$(PWD)/templates,target=/home/weaver/templates,readonly' \ - --mount 'type=bind,source=$(PWD)/src/main/java/io/opentelemetry/ibm/mq/metrics,target=/home/weaver/target' \ - ${WEAVER_CONTAINER} registry generate \ - --registry=/home/weaver/model \ - java \ - --future \ - /home/weaver/target - -.PHONY: generate-yaml -generate-yaml: - $(DOCKER_RUN) --rm \ - $(DOCKER_USER_IS_HOST_USER_ARG) \ - --mount 'type=bind,source=$(PWD)/model,target=/home/weaver/model,readonly' \ - --mount 'type=bind,source=$(PWD)/templates,target=/home/weaver/templates,readonly' \ - --mount 'type=bind,source=$(PWD)/,target=/home/weaver/target' \ - ${WEAVER_CONTAINER} registry generate \ - --registry=/home/weaver/model \ - yaml \ - --future \ - /home/weaver/target - -.PHONY: generate -generate: generate-docs generate-yaml generate-java diff --git a/ibm-mq-metrics/README.md b/ibm-mq-metrics/README.md index ced5c3530..5504c7e6a 100644 --- a/ibm-mq-metrics/README.md +++ b/ibm-mq-metrics/README.md @@ -85,14 +85,39 @@ java \ ## Generate code with Weaver -Weaver generates code, documentation and configuration for this program. +Weaver generates code, documentation and configuration for this program from the OpenTelemetry +semantic convention models in the `model/` directory. + +### Generate all artifacts (Java, docs, YAML) + +```shell +../gradlew :ibm-mq-metrics:weaverGenerate +``` + +### Generate specific artifacts ```shell -make generate +# Generate Java code only (automatically formatted with spotless) +../gradlew :ibm-mq-metrics:weaverGenerateJava + +# Generate markdown documentation +../gradlew :ibm-mq-metrics:weaverGenerateDocs + +# Generate YAML configuration +../gradlew :ibm-mq-metrics:weaverGenerateYaml + +# Check weaver model for errors +../gradlew :ibm-mq-metrics:weaverCheck ``` -This generates `config.yaml`, the `docs` folder, the `src/main/java/io/opentelemetry/ibm/mq/metrics` -Java code folder. +The generation produces: + +- `src/main/java/io/opentelemetry/ibm/mq/metrics/*.java` - Generated Java code (attributes, metrics, config) +- `docs/metrics.md` - Markdown documentation for all metrics +- `config.yml` - YAML configuration template + +**Note:** The Java code generation automatically runs `spotlessJavaApply` to ensure the generated +code follows the project's code style guidelines. ## Connection diff --git a/ibm-mq-metrics/build.gradle.kts b/ibm-mq-metrics/build.gradle.kts index 7beadbc20..252088ba9 100644 --- a/ibm-mq-metrics/build.gradle.kts +++ b/ibm-mq-metrics/build.gradle.kts @@ -3,12 +3,17 @@ plugins { id("com.gradleup.shadow") id("otel.java-conventions") id("otel.publish-conventions") + id("otel.weaver-conventions") } description = "IBM-MQ metrics" otelJava.moduleName.set("io.opentelemetry.contrib.ibm-mq-metrics") application.mainClass.set("io.opentelemetry.ibm.mq.opentelemetry.Main") +otelWeaver { + javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics") +} + val ibmClientJar: Configuration by configurations.creating { isCanBeResolved = true isCanBeConsumed = false diff --git a/ibm-mq-metrics/config.yml b/ibm-mq-metrics/config.yml index 603330f65..8d1e06df2 100644 --- a/ibm-mq-metrics/config.yml +++ b/ibm-mq-metrics/config.yml @@ -200,6 +200,8 @@ metrics: enabled: true "ibm.mq.manager.max.handles": # Max open handles enabled: true + "ibm.mq.connection.errors": # Number of connection errors + enabled: true sslConnection: trustStorePath: "" @@ -210,4 +212,4 @@ sslConnection: # Configure the OTLP exporter using system properties keys following the specification https://opentelemetry.io/docs/languages/java/configuration/ otlpExporter: - otel.exporter.otlp.endpoint: http://localhost:4318 + otel.exporter.otlp.endpoint: http://localhost:4318 \ No newline at end of file diff --git a/ibm-mq-metrics/weaver.Dockerfile b/ibm-mq-metrics/weaver.Dockerfile deleted file mode 100644 index 92fcad00e..000000000 --- a/ibm-mq-metrics/weaver.Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -# DO NOT BUILD -# This file is just for tracking dependencies of the semantic convention build. -# Dependabot can keep this file up to date with latest containers. - -# Weaver is used to generate markdown docs, and enforce policies on the model and run integration tests. -FROM otel/weaver:v0.18.0@sha256:5425ade81dc22ddd840902b0638b4b6a9186fb654c5b50c1d1ccd31299437390 AS weaver \ No newline at end of file