Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins {

id("io.github.gradle-nexus.publish-plugin")
id("otel.spotless-conventions")
id("otel.weaver-docs")
/* workaround for
What went wrong:
Could not determine the dependencies of task ':smoke-tests-otel-starter:spring-boot-3.2:bootJar'.
Expand Down
127 changes: 127 additions & 0 deletions conventions/src/main/kotlin/otel.weaver-docs.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import java.io.File

// Weaver documentation generation convention plugin
// Recursively finds all instrumentation modules with models/ directories
// and creates Gradle tasks to generate documentation using the OpenTelemetry Weaver tool

/**
* Creates a Gradle task for generating weaver documentation from a models directory.
*
* @param instrumentationName The name to use in the task (derived from relative path)
* @param modelsDir The directory containing weaver model files
* @param docsDir The output directory for generated documentation
*/
fun createWeaverDocTask(instrumentationName: String, modelsDir: File, docsDir: File): TaskProvider<Exec> {
return tasks.register<Exec>("generateWeaverDocs-${instrumentationName}") {
group = "documentation"
description = "Generate weaver documentation for $instrumentationName instrumentation"

inputs.dir(modelsDir)
outputs.dir(docsDir)

standardOutput = System.out
executable = "docker"

// Run as root in container to avoid permission issues with cache directory
val dockerArgs = listOf<String>()

val cacheDir = File(project.layout.buildDirectory.get().asFile, ".weaver-cache")

// Template hierarchy (in order of precedence):
// 1. Module-specific templates: models/templates/ (highest priority)
// 2. Global shared templates: weaver-templates/ (medium priority)
// 3. Default semantic-conventions templates (fallback)
val moduleTemplatesDir = File(modelsDir, "templates")
val globalTemplatesDir = File(project.rootDir, "weaver-templates")

val templatesSource = when {
moduleTemplatesDir.exists() && moduleTemplatesDir.isDirectory -> {
// Use module-specific templates (no mount needed, already in /source/templates)
"/source/templates"
}
globalTemplatesDir.exists() && globalTemplatesDir.isDirectory -> {
// Use global shared templates (needs separate mount)
"/shared-templates"
}
else -> {
// Fall back to default semantic-conventions templates
"https://github.com/open-telemetry/semantic-conventions/archive/refs/tags/v1.34.0.zip[templates]"
}
}

// Build mount arguments - add global templates mount if using them
val mountArgs = mutableListOf(
"--mount", "type=bind,source=${modelsDir.absolutePath},target=/source,readonly",
"--mount", "type=bind,source=${docsDir.absolutePath},target=/target",
"--mount", "type=bind,source=${cacheDir.absolutePath},target=/home/weaver/.weaver"
)

if (templatesSource == "/shared-templates") {
mountArgs.addAll(listOf(
"--mount", "type=bind,source=${globalTemplatesDir.absolutePath},target=/shared-templates,readonly"
))
}

val weaverArgs = listOf(
"otel/weaver:v0.18.0@sha256:5425ade81dc22ddd840902b0638b4b6a9186fb654c5b50c1d1ccd31299437390",
"registry", "generate",
"--registry=/source",
"--templates=${templatesSource}",
"markdown", "/target"
)

args = listOf("run", "--rm", "--platform=linux/x86_64") + dockerArgs + mountArgs + weaverArgs

doFirst {
if (!modelsDir.exists()) {
throw GradleException("Models directory does not exist: ${modelsDir.absolutePath}")
}
docsDir.mkdirs()
cacheDir.mkdirs()
}
}
}

/**
* Recursively searches for all models/ directories under a given directory.
*
* @param dir The directory to search
* @param baseDir The base directory for calculating relative paths
* @return List of pairs containing (task-suffix, module-directory)
*/
fun findModelsDirectories(dir: File, baseDir: File = dir): List<Pair<String, File>> {
val results = mutableListOf<Pair<String, File>>()

dir.listFiles()?.forEach { file ->
if (file.isDirectory) {
val modelsDir = File(file, "models")
if (modelsDir.exists() && modelsDir.isDirectory) {
val relativePath = file.relativeTo(baseDir).path.replace(File.separatorChar, '-')
results.add(Pair(relativePath, file))
}
// Recursively search subdirectories
results.addAll(findModelsDirectories(file, baseDir))
}
}

return results
}

// Find all instrumentation modules with models directories and register tasks
val weaverDocTasks = mutableListOf<TaskProvider<Exec>>()
val instrumentationDir = file("instrumentation")
if (instrumentationDir.exists() && instrumentationDir.isDirectory) {
findModelsDirectories(instrumentationDir).forEach { (taskSuffix, moduleDir) ->
val modelsDir = File(moduleDir, "models")
val docsDir = File(moduleDir, "docs")
val taskProvider = createWeaverDocTask(taskSuffix, modelsDir, docsDir)
weaverDocTasks.add(taskProvider)
}
}

// Create an aggregate task to generate all weaver docs
tasks.register("generateAllWeaverDocs") {
group = "documentation"
description = "Generate weaver documentation for all instrumentation modules"
dependsOn(weaverDocTasks)
}
70 changes: 70 additions & 0 deletions docs/contributing/weaver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Weaver Documentation Generation Guide

This project now includes Gradle tasks for generating documentation from weaver model files using
the OpenTelemetry Weaver tool. The model files are currently generated by the gradle task
`./gradlew :instrumentation-docs:runAnalysis`.

The build system automatically finds all instrumentation modules with a `models/` directory and
creates Gradle tasks to generate documentation into a corresponding `docs/` directory.

## Model Directory Structure

Each instrumentation with weaver support will have:

```
instrumentation/<module-name>/
├── models/ # Scaffolded / Generated by the DocGenerater task
│ ├── registry_manifest.yaml # Registry metadata and dependencies
│ ├── signals.yaml # Metrics and spans definitions
│ └── attributes.yaml # Attribute group definitions
└── docs/ # Generated documentation goes here
└── signals.md # Auto-generated
```

## Usage

### Generate Documentation for a Specific Module

```bash
./gradlew generateWeaverDocs-<module-name>

# Example:
./gradlew generateWeaverDocs-activej-http-6.0
```

### Generate Documentation for All Modules

```bash
./gradlew generateAllWeaverDocs
```

### List Available Documentation Tasks

```bash
./gradlew tasks --group=documentation
```

## Requirements

- Docker must be installed and running
- The otel/weaver docker image will be pulled automatically

## Output

Generated documentation will appear in:
- `instrumentation/<module-name>/docs/signals.md`

## Manually Adding Weaver Support to a New Module

1. Create a `models/` directory in your instrumentation module
2. Add `registry_manifest.yaml` with metadata and dependencies
3. Add `signals.yaml` to define metrics/spans
4. Add `attributes.yaml` for attributes
5. Run `./gradlew generateWeaverDocs-<your-module-name>`

The build system will automatically detect the new models directory and create the task.

## Further Reading

- [Weaver Documentation](https://github.com/open-telemetry/weaver/tree/main/docs)
- [Weaver Example Project](https://github.com/jerbly/weaver-example)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.instrumentation.docs;

import static io.opentelemetry.instrumentation.docs.WeaverModelGenerator.generateWeaverModels;
import static java.util.Locale.Category.FORMAT;

import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
Expand Down Expand Up @@ -47,6 +48,8 @@ public static void main(String[] args) throws IOException {
YamlHelper.generateInstrumentationYaml(modules, writer);
}

generateWeaverModels(modules);

printStats(modules);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.docs;

import static java.nio.charset.StandardCharsets.UTF_8;

import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public class WeaverModelGenerator {

private static final Path baseRepoPath = getPath();

private static Path getPath() {
String base = System.getProperty("basePath");
if (base == null || base.isBlank()) {
return Paths.get(".");
}
return Paths.get(base);
}

public static void generateWeaverModels(List<InstrumentationModule> modules) throws IOException {
for (InstrumentationModule module : modules) {
if (!module.getInstrumentationName().equals("activej-http-6.0")) {
continue;
}

Path moduleSrc = baseRepoPath.resolve(module.getSrcPath());
Path modelsDir = moduleSrc.resolve("models");
Files.createDirectories(modelsDir);

Path signalsPath = modelsDir.resolve("signals.yaml");
try (BufferedWriter writer = Files.newBufferedWriter(signalsPath, UTF_8)) {
generateSignals(module, writer);
}

Path manifestPath = modelsDir.resolve("registry_manifest.yaml");
try (BufferedWriter writer = Files.newBufferedWriter(manifestPath, UTF_8)) {
generateManifest(module, writer);
}

Set<String> attributes = getMetricAttributes(module);
if (!attributes.isEmpty()) {
Path attributesPath = modelsDir.resolve("attributes.yaml");
try (BufferedWriter writer = Files.newBufferedWriter(attributesPath, UTF_8)) {
generateAttributes(module, writer, attributes);
}
}
}
}

public static void generateSignals(InstrumentationModule module, BufferedWriter writer)
throws IOException {
writer.write("# This file is generated and should not be manually edited.\n");
writer.write("groups:\n");

Map<String, List<EmittedMetrics.Metric>> metricsMap = module.getMetrics();
if (metricsMap != null && metricsMap.get("default") != null) {
for (EmittedMetrics.Metric metric : metricsMap.get("default")) {
writer.write(" - id: metric." + quote(metric.getName()) + "\n");
writer.write(" type: metric\n");
writer.write(" metric_name: " + quote(metric.getName()) + "\n");
writer.write(" stability: development\n");
writer.write(" brief: " + quote(metric.getDescription()) + "\n");
writer.write(" instrument: " + quote(metric.getType().toLowerCase(Locale.ROOT)) + "\n");
writer.write(" unit: " + quote(metric.getUnit()) + "\n");
writer.write(" attributes:\n");
for (TelemetryAttribute attribute : metric.getAttributes()) {
writer.write(" - ref: " + quote(attribute.getName()) + "\n");
}
}
}
}

public static void generateManifest(InstrumentationModule module, BufferedWriter writer)
throws IOException {
writer.write("# This file is generated and should not be manually edited.\n");
writer.write("name: " + quote(module.getInstrumentationName()) + "\n");
writer.write(
"description: " + quote(module.getInstrumentationName() + " Semantic Conventions") + "\n");
writer.write("semconv_version: 0.1.0\n");
writer.write("schema_base_url: https://weaver-example.io/schemas/\n");
writer.write("dependencies:\n");
writer.write(" - name: otel\n");
writer.write(
" registry_path: https://github.com/open-telemetry/semantic-conventions/archive/refs/tags/v1.34.0.zip[model]");
}

public static void generateAttributes(
InstrumentationModule module, BufferedWriter writer, Set<String> attributes)
throws IOException {
writer.write("# This file is generated and should not be manually edited.\n");
writer.write("groups:\n");
writer.write(" - id: registry." + module.getInstrumentationName() + "\n");
writer.write(" type: attribute_group\n");
writer.write(" display_name: " + module.getResolvedName() + " Attributes\n");
writer.write(
" brief: Attributes captured by " + module.getResolvedName() + " instrumentation.\n");
writer.write(" attributes:\n");
for (String attribute : attributes) {
writer.write(" - ref: " + attribute + "\n");
}
}

/**
* Get all metric attributes used by the given module. Sorted and deduplicated.
*
* @param module the instrumentation module
* @return set of attribute names
*/
// visible for testing
public static Set<String> getMetricAttributes(InstrumentationModule module) {
Set<String> attributes = new java.util.TreeSet<>();
if (module.getMetrics() != null && module.getMetrics().get("default") != null) {
for (EmittedMetrics.Metric metric : module.getMetrics().get("default")) {
for (TelemetryAttribute attribute : metric.getAttributes()) {
String name = attribute.getName();
if (name != null) {
attributes.add(name);
}
}
}
}
return attributes;
}

private static String quote(String s) {
if (s == null) {
return "\"\"";
}
return "\"" + s.replace("\"", "\\\"") + "\"";
}

private WeaverModelGenerator() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ public String getInstrumentationName() {
return instrumentationName;
}

public String getResolvedName() {
if (metadata != null && metadata.getDisplayName() != null) {
return metadata.getDisplayName();
}
return instrumentationName;
}

public String getNamespace() {
return namespace;
}
Expand Down
Loading
Loading