Skip to content
Open
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
154 changes: 102 additions & 52 deletions azure-functions-java-opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

# Azure Functions OpenTelemetry Integration (Java)

This library integrates [OpenTelemetry](https://opentelemetry.io/) with Java-based Azure Functions. It automatically:
This library provides [OpenTelemetry](https://opentelemetry.io/) integration for Java-based Azure Functions when running with an OpenTelemetry agent. It automatically:

1. Initializes the OpenTelemetry SDK on-demand.
2. Merges resource attributes for Azure Functions (e.g., function name, region, resource group).
3. Optionally configures Azure Monitor if the environment variable `APPLICATIONINSIGHTS_CONNECTION_STRING` is set.
4. Provides a middleware that creates a new span for each function invocation.
1. Creates spans for each function invocation with proper trace context propagation.
2. Provides convenient helper methods for creating custom spans.

**Note:** This library requires an OpenTelemetry Java agent to be present. It does not initialize or configure the OpenTelemetry SDK itself.

---

## Contents

* [Key Classes](#key-classes)
* [Installation](#installation)
* [Prerequisites](#prerequisites)
* [How It Works](#how-it-works)
* [Azure Monitor Integration (Optional)](#azure-monitor-integration-optional)
* [Usage in Azure Functions](#usage-in-azure-functions)
* [Local Development](#local-development)
* [Testing](#testing)
Expand All @@ -26,20 +26,12 @@ This library integrates [OpenTelemetry](https://opentelemetry.io/) with Java-bas
## Key Classes

1. **`FunctionsOpenTelemetry`**
* Provides helper methods like `startSpan(...)` which work with the global OpenTelemetry instance configured by the agent.
* Accepts either an OpenTelemetry `Context` or an Azure Functions `TraceContext`.

* Provides a static `sdk()` method to lazily initialize the SDK.
* Offers helper methods like `startSpan(...)` which accept either an OpenTelemetry `Context` or an Azure Functions `TraceContext`.

2. **`FunctionsResourceDetector`**

* Detects standard Azure Functions environment variables (e.g. `WEBSITE_SITE_NAME`) and builds resource attributes like `service.name`, `cloud.provider`, etc.
* Fallbacks to `java-function-app` for `service.name` if not running on Azure.

3. **`OpenTelemetryInvocationMiddleware`**

* Implements Azure Functions middleware (`com.microsoft.azure.functions.internal.spi.middleware.Middleware`).
* Initializes a static OTel SDK during when it is loaded in by the Java Worker during Worker Initialization.
* Starts a span on each function invocation and propagates existing trace context from the Azure Functions host.
2. **`OpenTelemetryInvocationMiddleware`**
* Implements Azure Functions middleware (`com.microsoft.azure.functions.internal.spi.middleware.Middleware`).
* Starts a span for each function invocation and propagates trace context from the Azure Functions host.

---

Expand All @@ -59,62 +51,120 @@ Add the library to your `pom.xml`:

---

## How It Works
## Prerequisites

1. **Lazy Initialization**
When you first call `FunctionsOpenTelemetry.sdk()`, it constructs an `OpenTelemetrySdk` via `AutoConfiguredOpenTelemetrySdk.builder()`.
This library requires an OpenTelemetry Java agent to be present and configured. The agent handles:

* Merges Azure Functions resource attributes.
* Registers a shutdown hook to cleanly close the tracer provider on JVM exit.
* OpenTelemetry SDK initialization and configuration
* Exporter setup (e.g., OTLP, Azure Monitor, Jaeger)
* Instrumentation of HTTP clients, databases, etc.

2. **Span Creation**
To run your Azure Functions with the agent:

* `FunctionsOpenTelemetry.startSpan(...)` can start spans for custom logic.
* `OpenTelemetryInvocationMiddleware` automatically starts a span for each function invocation and tags it with `faas.invocation_id` and `faas.name`.
```bash
# Download the OpenTelemetry Java agent
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

---
# Run with the agent
func start --java-options="-javaagent:opentelemetry-javaagent.jar"
```

For Azure deployment, configure the agent via application settings:
```
JAVA_OPTS=-javaagent:opentelemetry-javaagent.jar
OTEL_SERVICE_NAME=my-function-app
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector-endpoint
```

## Azure Monitor Integration (Optional)
---

If you set `APPLICATIONINSIGHTS_CONNECTION_STRING` and also set `JAVA_APPLICATIONINSIGHTS_ENABLE_TELEMETRY=true`, this library tries to reflectively call `com.azure.monitor.opentelemetry.autoconfigure.AzureMonitorAutoConfigure`.
## How It Works

* If that class is found, Azure Monitor is configured to export traces, metrics, and logs.
* If not present, the initialization logs a warning and continues without Azure Monitor.
1. **Agent Detection**
The library checks if an OpenTelemetry agent has configured the global OpenTelemetry instance. If no agent is detected, it throws an `IllegalStateException`.

This approach avoids a hard compile-time dependency on the Azure Monitor library.
2. **Span Creation**
* `OpenTelemetryInvocationMiddleware` automatically creates a span for each function invocation with `faas.invocation_id` and `faas.name` attributes.
* `FunctionsOpenTelemetry.startSpan(...)` provides convenient methods for creating custom spans.

---

## Usage in Azure Functions

1. **Middleware Registration**

* Azure Functions Java Worker auto-discovers middleware. Make sure `OpenTelemetryInvocationMiddleware` is on your classpath.
* Once loaded, each invocation automatically has a new span, parented by any incoming trace context from the host.
1. **Automatic Middleware**
The middleware is automatically registered and creates spans for all function invocations.

2. **Custom Spans**

```java
Span customSpan = FunctionsOpenTelemetry.startSpan(
"myTracer",
"mySpan",
someTraceContext, // or null
SpanKind.INTERNAL // or null -> defaults to INTERNAL
);
try (Scope scope = customSpan.makeCurrent()) {
// do your work
} finally {
customSpan.end();
import com.microsoft.azure.functions.opentelemetry.FunctionsOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Scope;

@FunctionName("MyFunction")
public HttpResponseMessage run(
@HttpTrigger(name = "req", methods = {HttpMethod.GET}) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) {

// Create a custom span
Span customSpan = FunctionsOpenTelemetry.startSpan(
"business-logic",
context.getTraceContext(),
SpanKind.INTERNAL
);

try (Scope scope = customSpan.makeCurrent()) {
// Your business logic here
customSpan.setAttribute("user.id", "12345");
return request.createResponseBuilder(HttpStatus.OK).body("Hello World").build();
} finally {
customSpan.end();
}
}
```

---

## Local Development

* If `WEBSITE_SITE_NAME` is not set, `FunctionsResourceDetector` defaults `service.name` to `"java-function-app"`.
* All other Azure-specific attributes (e.g., region, resource group) remain unset if the corresponding environment variables do not exist.
* You do **not** need to set Azure Monitor or any other exporters unless you want local telemetry exporting.
* Make sure to run with the OpenTelemetry agent even during local development.
* Configure the agent with appropriate exporters for local testing (e.g., console exporter).

---

## Testing

This repo includes unit tests under `src/test/java/...`. Key points:

1. **Agent Simulation**
Tests may need to mock the global OpenTelemetry instance since the agent won't be present during unit testing.

2. **Disabling Default Exporters**
We disable exporters in tests to avoid configuration issues:

```java
@BeforeAll
static void setUp() {
System.setProperty("otel.metrics.exporter", "none");
System.setProperty("otel.traces.exporter", "none");
System.setProperty("otel.logs.exporter", "none");
}
```

3. **Middleware Testing**
We use [Mockito](https://site.mockito.org/) to mock `MiddlewareContext` and verify the middleware behavior.

---

## Migration from SDK-based Versions

If you were using a previous version that initialized its own SDK:

1. **Remove manual initialization** - Delete any calls to `FunctionsOpenTelemetry.initialize()`
2. **Add OpenTelemetry agent** - Configure your deployment to use the OpenTelemetry Java agent
3. **Move configuration to agent** - Move exporter and instrumentation configuration to agent properties (environment variables starting with `OTEL_`)

The span creation APIs remain unchanged, so your custom instrumentation code should continue to work.

---

Expand Down Expand Up @@ -151,4 +201,4 @@ This repo includes simple unit tests under `src/test/java/...`. Key points:
This allows verifying resource attributes without polluting global environment variables.

3. **Middleware Testing**
We use [Mockito](https://site.mockito.org/) to mock `MiddlewareContext` and confirm `OpenTelemetryInvocationMiddleware` properly invokes the chain and handles exceptions.
We use [Mockito](https://site.mockito.org/) to mock `MiddlewareContext` and confirm `OpenTelemetryInvocationMiddleware` properly invokes the chain and handles exceptions.
64 changes: 55 additions & 9 deletions azure-functions-java-opentelemetry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@

<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-opentelemetry</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
<packaging>jar</packaging>

<name>Microsoft Azure Functions Java OpenTelemetry Support</name>
<description>This package contains classes/interfaces for advanced SDK-based type bindings for Azure Functions Java Worker.</description>
<description>OpenTelemetry integration for Azure Functions Java runtime. Requires an OpenTelemetry agent.</description>
<url>https://azure.microsoft.com/en-us/services/functions</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>5.13.0</junit.jupiter.version>
<junit.jupiter.version>5.9.3</junit.jupiter.version>
</properties>

<licenses>
Expand All @@ -33,9 +33,6 @@
<distribution>repo</distribution>
</license>
</licenses>



<dependencies>
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
Expand All @@ -49,17 +46,16 @@
<version>1.1.0</version>
<scope>provided</scope>
</dependency>
<!-- OpenTelemetry dependencies - these will be shaded into our JAR -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.49.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
<artifactId>opentelemetry-sdk-common</artifactId>
<version>1.49.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -73,6 +69,18 @@
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand All @@ -95,6 +103,44 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M8</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>io.opentelemetry</pattern>
<shadedPattern>com.microsoft.azure.functions.opentelemetry.shaded.otel</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<createDependencyReducedPom>true</createDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Build output
target/
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

# IDE
.idea/
*.iml
.settings/
.project
.classpath

# macOS
.DS_Store

# Azure Functions
local.settings.json
bin/
obj/
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"vscjava.vscode-java-debug"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Java Functions",
"type": "java",
"request": "attach",
"hostName": "127.0.0.1",
"port": 5005,
"preLaunchTask": "func: host start"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"azureFunctions.javaBuildTool": "maven",
"azureFunctions.deploySubpath": "target/azure-functions/java-otel-cascade-1756925571105",
"azureFunctions.projectLanguage": "Java",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "package (functions)"
}
Loading