diff --git a/instrumentation/java/azure-functions/README.md b/instrumentation/java/azure-functions/README.md index 862a2a8..6e526f1 100644 --- a/instrumentation/java/azure-functions/README.md +++ b/instrumentation/java/azure-functions/README.md @@ -20,121 +20,153 @@ If you just want to build and deploy the example, feel free to skip this section The application used for this example is a simple Hello World application. -We added a helper class named [SplunkTelemetryConfiguration](./SplunkTelemetryConfigurator.cs), and included code to -assist with initializing the tracer, as well as a custom logger to inject the trace context. - -The tracer initialization is based on the example found in -[Instrument Java Azure functions for Splunk Observability Cloud](https://docs.splunk.com/observability/en/gdi/get-data-in/serverless/azure/instrument-azure-functions-java.html): +We added a helper class named [SplunkTelemetryConfiguration](./src/main/java/com/function/SplunkTelemetryConfigurator.java), and included code to assist with initializing the OpenTelemetry SDK: ```` -public static TracerProvider ConfigureSplunkTelemetry() -{ - // Get environment variables from function configuration - // You need a valid Splunk Observability Cloud access token and realm - var serviceName = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME") ?? "Unknown"; - var accessToken = Environment.GetEnvironmentVariable("SPLUNK_ACCESS_TOKEN")?.Trim(); - var realm = Environment.GetEnvironmentVariable("SPLUNK_REALM")?.Trim(); - - ArgumentNullException.ThrowIfNull(accessToken, "SPLUNK_ACCESS_TOKEN"); - ArgumentNullException.ThrowIfNull(realm, "SPLUNK_REALM"); - - var builder = Sdk.CreateTracerProviderBuilder() - // Use Add[instrumentation-name]Instrumentation to instrument missing services - // Use Nuget to find different instrumentation libraries - .AddHttpClientInstrumentation(opts => - { - // This filter prevents background (parent-less) http client activity - opts.FilterHttpWebRequest = req => Activity.Current?.Parent != null; - opts.FilterHttpRequestMessage = req => Activity.Current?.Parent != null; - }) - // Use AddSource to add your custom DiagnosticSource source names - //.AddSource("My.Source.Name") - // Creates root spans for function executions - .AddSource("Microsoft.Azure.Functions.Worker") - .SetSampler(new AlwaysOnSampler()) - .ConfigureResource(configure => configure - .AddService(serviceName: serviceName, serviceVersion: "1.0.0") - // See https://github.com/open-telemetry/opentelemetry-java-contrib/tree/main/src/OpenTelemetry.Resources.Azure - // for other types of Azure detectors - .AddAzureAppServiceDetector()) - .AddOtlpExporter(opts => - { - opts.Endpoint = new Uri($"https://ingest.{realm}.signalfx.com/v2/trace/otlp"); - opts.Protocol = OtlpExportProtocol.HttpProtobuf; - opts.Headers = $"X-SF-TOKEN={accessToken}"; - }) - // Add the console exporter, which is helpful for debugging as the - // spans get written to the console but should be removed in production - .AddConsoleExporter(); - - return builder.Build()!; + public static OpenTelemetry configureOpenTelemetry() { + + String serviceName = System.getenv("OTEL_SERVICE_NAME"); + String deploymentEnvironment = System.getenv("DEPLOYMENT_ENVIRONMENT"); + String realm = System.getenv("SPLUNK_REALM"); + String accessToken = System.getenv("SPLUNK_ACCESS_TOKEN"); + + if (serviceName == null) + throw new IllegalArgumentException("The OTEL_SERVICE_NAME environment variable must be populated"); + if (deploymentEnvironment == null) + throw new IllegalArgumentException("The DEPLOYMENT_ENVIRONMENT environment variable must be populated"); + if (realm == null) + throw new IllegalArgumentException("The SPLUNK_REALM environment variable must be populated"); + if (accessToken == null) + throw new IllegalArgumentException("The SPLUNK_ACCESS_TOKEN environment variable must be populated"); + + // Note: an Azure resource detector isn't currently available but should be + // added here once it is + Resource resource = Resource + .getDefault() + .toBuilder() + .put(ResourceAttributes.SERVICE_NAME, serviceName) + .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, deploymentEnvironment) + .build(); + + OtlpHttpSpanExporter spanExporter = OtlpHttpSpanExporter.builder() + .setEndpoint(String.format("https://ingest.%s.signalfx.com/v2/trace/otlp", realm)) + .addHeader("X-SF-TOKEN", accessToken) + .build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); } ```` -The custom logger injects the trace context as follows: +It requires the following dependencies to be added to the [pom.xml](./pom.xml) file: ```` - public static ILogger ConfigureLogger() - { - var loggerFactory = LoggerFactory.Create(logging => - { - logging.ClearProviders(); // Clear existing providers - logging.Configure(options => - { - options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId - | ActivityTrackingOptions.TraceId - | ActivityTrackingOptions.ParentId - | ActivityTrackingOptions.Baggage - | ActivityTrackingOptions.Tags; - }).AddConsole(options => - { - options.FormatterName = "splunkLogsJson"; - }); - logging.AddConsoleFormatter(); - }); + + + + io.opentelemetry + opentelemetry-bom + 1.44.1 + pom + import + + + + + + ... + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + + io.opentelemetry.semconv + opentelemetry-semconv + 1.28.0-alpha + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry.instrumentation + opentelemetry-log4j-context-data-2.17-autoconfigure + 2.8.0-alpha + runtime + + - return loggerFactory.CreateLogger(); - } ```` -The [Program.cs file](./Program.cs) was then modified to configure -OpenTelemetry using the helper class as follows: - -```` -using OpenTelemetry.Trace; -using SplunkTelemetry; +Note that we've added `opentelemetry-log4j-context-data-2.17-autoconfigure` as a dependency as well, which injects the trace ID and span ID from an active span into Log4j's context data. Refer to [ContextData Instrumentation for Log4j2]https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure) +for further details. -var tracerProvider = SplunkTelemetryConfigurator.ConfigureSplunkTelemetry(); -var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureServices(services => services.AddSingleton(tracerProvider)) - .Build(); -```` +The log4j2.xml configuration file was modified to utilize this trace context and add it to the log output: -And then the [Azure function](./azure_function_java8_opentelemetry_example.cs) was modified to configure -the logger used by the application: ```` - public azure_function_java8_opentelemetry_example(ILogger logger) - { - _logger = SplunkTelemetryConfigurator.ConfigureLogger(); - } + + + + %d %5p [%t] %c{3} - trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} service.name=${env:OTEL_SERVICE_NAME} %m%n + + + ```` -These code changes required a number of packages to be added to the azure-functions.csproj file: +The [Function.java file](./src/main/java/com/function/Function.java) was then modified to configure +OpenTelemetry using the helper class as follows: ```` - - ... - - - - - - +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; + +public class Function { + + private final OpenTelemetry openTelemetry = SplunkTelemetryConfigurator.configureOpenTelemetry(); + private final Tracer tracer = openTelemetry.getTracer(Function.class.getName(), "0.1.0"); + private static final Logger logger = LogManager.getLogger(Function.class); + + @FunctionName("Hello") + public HttpResponseMessage run( + @HttpTrigger( + name = "req", + methods = {HttpMethod.GET, HttpMethod.POST}, + authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage> request, + final ExecutionContext context) { + + Span span = tracer.spanBuilder("helloFunction").startSpan(); + + try (Scope scope = span.makeCurrent()) { + logger.info("Handling the Hello function call"); + return request.createResponseBuilder(HttpStatus.OK).body("Hello, World!").build(); + } + catch (Throwable t) { + span.recordException(t); + return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("An error occurred while processing the request").build(); + } + finally { + span.end(); + } + } ```` -The `local.settings.json` file was then updated to include the Splunk realm and access token which is +The `local.settings.json` file was then updated to include the service name, deployment environment, Splunk realm and access token which is used for local testing: ```` @@ -142,13 +174,18 @@ used for local testing: "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "", - "FUNCTIONS_WORKER_RUNTIME": "java-isolated", + "FUNCTIONS_WORKER_RUNTIME": "java", + "APPLICATIONINSIGHTS_ENABLE_AGENT": "true", + "OTEL_SERVICE_NAME": "azure-function-java-opentelemetry-example", + "DEPLOYMENT_ENVIRONMENT": "test", "SPLUNK_REALM": "", "SPLUNK_ACCESS_TOKEN": "" } } ```` +Note that we also set APPLICATIONINSIGHTS_ENABLE_AGENT to "true" to ensure the log4j logs appear in App Insights. + ## Build and Deploy Open the following project using Visual Studio Code: @@ -167,12 +204,7 @@ with Java 21 as the runtime. ### Create a Deployment Slot (Optional) -By default, Azure will use a deployment slot named "Production" for an Azure Function App. -This results in OpenTelemetry using a `deployment.environment` setting of "Production" as well, -which may not be desired. - -To use a different `deployment.environment` value, we can create a different deployment slot instead. - +By default, Azure will use a deployment slot named "Production" for an Azure Function App. In my example, I created a deployment slot named "test". ![Deployment Slot](./images/deployment-slot.png) @@ -180,8 +212,7 @@ In my example, I created a deployment slot named "test". ### Set Environment Variables To allow OpenTelemetry to send trace data to Splunk Observability Cloud, -we need to set the SPLUNK_REALM and SPLUNK_ACCESS_TOKEN environment variables -for our Azure Function App: +we need to set the APPLICATIONINSIGHTS_ENABLE_AGENT, OTEL_SERVICE_NAME, DEPLOYMENT_ENVIRONMENT, SPLUNK_REALM and SPLUNK_ACCESS_TOKEN environment variables for our Azure Function App: ![Environment Variables](./images/env-vars.png) @@ -231,18 +262,7 @@ using the custom logging changes described above: ```` { - "event_id": 0, - "log_level": "information", - "category": "example.azure_function_java8_opentelemetry_example", - "message": "C# HTTP trigger function processed a request.", - "timestamp": "2024-12-03T23:18:17.2770657Z", - "service.name": "opentelemetry-examples", - "severity": "INFO", - "span_id": "c6667cb0450822dd", - "trace_id": "c5580e362f333788634779f64220a087", - "parent_id": "2c06698f7f40edb8", - "tag_az.schema_url": "https://opentelemetry.io/schemas/1.17.0", - "tag_faas.execution": "25fc5264-f946-4c63-b561-822b7c2ccddd" + 2024-12-05 13:48:58,320 INFO [pool-2-thread-1] com.function.Function - trace_id=746bf6fd39b1c76cb587ed5ca29c4d8a span_id=0f93d5fb716a48e9 trace_flags=01 service.name=azure-function-java-opentelemetry-example Handling the Hello function call } ```` diff --git a/instrumentation/java/azure-functions/images/trace.png b/instrumentation/java/azure-functions/images/trace.png new file mode 100644 index 0000000..52b7a27 Binary files /dev/null and b/instrumentation/java/azure-functions/images/trace.png differ diff --git a/instrumentation/java/azure-functions/pom.xml b/instrumentation/java/azure-functions/pom.xml index 8e97085..0521a1e 100644 --- a/instrumentation/java/azure-functions/pom.xml +++ b/instrumentation/java/azure-functions/pom.xml @@ -36,17 +36,22 @@ ${azure.functions.java.library.version} - io.opentelemetry - opentelemetry-api + org.apache.logging.log4j + log4j-api + 2.24.2 + + + org.apache.logging.log4j + log4j-core + 2.24.2 io.opentelemetry - opentelemetry-sdk + opentelemetry-api io.opentelemetry - opentelemetry-exporter-logging - 1.44.1 + opentelemetry-sdk @@ -58,6 +63,12 @@ io.opentelemetry opentelemetry-exporter-otlp + + io.opentelemetry.instrumentation + opentelemetry-log4j-context-data-2.17-autoconfigure + 2.8.0-alpha + runtime + diff --git a/instrumentation/java/azure-functions/src/main/java/com/function/Function.java b/instrumentation/java/azure-functions/src/main/java/com/function/Function.java index 064abb5..42870fc 100644 --- a/instrumentation/java/azure-functions/src/main/java/com/function/Function.java +++ b/instrumentation/java/azure-functions/src/main/java/com/function/Function.java @@ -2,6 +2,9 @@ import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.microsoft.azure.functions.ExecutionContext; import com.microsoft.azure.functions.HttpMethod; import com.microsoft.azure.functions.HttpRequestMessage; @@ -20,6 +23,7 @@ public class Function { private final OpenTelemetry openTelemetry = SplunkTelemetryConfigurator.configureOpenTelemetry(); private final Tracer tracer = openTelemetry.getTracer(Function.class.getName(), "0.1.0"); + private static final Logger logger = LogManager.getLogger(Function.class); @FunctionName("Hello") public HttpResponseMessage run( @@ -32,11 +36,10 @@ public HttpResponseMessage run( Span span = tracer.spanBuilder("helloFunction").startSpan(); - // Make the span the current span try (Scope scope = span.makeCurrent()) { - context.getLogger().info("Handling the Hello function call"); + logger.info("Handling the Hello function call"); return request.createResponseBuilder(HttpStatus.OK).body("Hello, World!").build(); - } + } catch (Throwable t) { span.recordException(t); return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("An error occurred while processing the request").build(); diff --git a/instrumentation/java/azure-functions/src/main/java/com/function/SplunkTelemetryConfigurator.java b/instrumentation/java/azure-functions/src/main/java/com/function/SplunkTelemetryConfigurator.java index 0488639..db10183 100644 --- a/instrumentation/java/azure-functions/src/main/java/com/function/SplunkTelemetryConfigurator.java +++ b/instrumentation/java/azure-functions/src/main/java/com/function/SplunkTelemetryConfigurator.java @@ -3,7 +3,6 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; @@ -15,40 +14,34 @@ public class SplunkTelemetryConfigurator { public static OpenTelemetry configureOpenTelemetry() { - // Note: an Azure resource detector isn't currently available but should be - // added here once it is - Resource resource = Resource - .getDefault() - .toBuilder() - .put( - ResourceAttributes.SERVICE_NAME, - "azure-function-java-opentelemetry-example" - ) - .put( - ResourceAttributes.SERVICE_VERSION, - "0.1.0" - ) - .put( - ResourceAttributes.DEPLOYMENT_ENVIRONMENT, - "test" - ) - .build(); - + String serviceName = System.getenv("OTEL_SERVICE_NAME"); + String deploymentEnvironment = System.getenv("DEPLOYMENT_ENVIRONMENT"); String realm = System.getenv("SPLUNK_REALM"); String accessToken = System.getenv("SPLUNK_ACCESS_TOKEN"); + if (serviceName == null) + throw new IllegalArgumentException("The OTEL_SERVICE_NAME environment variable must be populated"); + if (deploymentEnvironment == null) + throw new IllegalArgumentException("The DEPLOYMENT_ENVIRONMENT environment variable must be populated"); if (realm == null) throw new IllegalArgumentException("The SPLUNK_REALM environment variable must be populated"); if (accessToken == null) throw new IllegalArgumentException("The SPLUNK_ACCESS_TOKEN environment variable must be populated"); + // Note: an Azure resource detector isn't currently available but should be + // added here once it is + Resource resource = Resource + .getDefault() + .toBuilder() + .put(ResourceAttributes.SERVICE_NAME, serviceName) + .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, deploymentEnvironment) + .build(); + OtlpHttpSpanExporter spanExporter = OtlpHttpSpanExporter.builder() .setEndpoint(String.format("https://ingest.%s.signalfx.com/v2/trace/otlp", realm)) .addHeader("X-SF-TOKEN", accessToken) .build(); - LoggingSpanExporter loggingSpanExporter = LoggingSpanExporter.create(); - SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) .setResource(resource) @@ -58,6 +51,5 @@ public static OpenTelemetry configureOpenTelemetry() { .setTracerProvider(sdkTracerProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); - } } diff --git a/instrumentation/java/azure-functions/src/main/resources/log4j2.xml b/instrumentation/java/azure-functions/src/main/resources/log4j2.xml new file mode 100644 index 0000000..b3785d5 --- /dev/null +++ b/instrumentation/java/azure-functions/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + %d %5p [%t] %c{3} - trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} service.name=${env:OTEL_SERVICE_NAME} %m%n + + + + + + + + + + + + \ No newline at end of file