Skip to content

Commit

Permalink
feat: add auto-instrumentation support for Java 8+ (#228)
Browse files Browse the repository at this point in the history
Signed-off-by: Michele Mancioppi <[email protected]>
  • Loading branch information
mmanciop authored Jan 7, 2025
1 parent d03892a commit 088b15c
Show file tree
Hide file tree
Showing 46 changed files with 1,878 additions and 250 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ updates:
development-dependencies:
dependency-type: "development"

# Dash0 OpenTelemetry Distro for Node.js
- package-ecosystem: "npm"
directory: "images/instrumentation/node.js/"
schedule:
interval: "daily"

# OpenTelemetry Java agent
- package-ecosystem: "maven"
directory: "images/instrumentation/jvm/"
schedule:
interval: "daily"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
injector_binary_and_instrumentation_image_tests:
name: Injector Binary & Instrumentation Image Tests
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 40
steps:
- uses: actions/checkout@v4

Expand Down
5 changes: 3 additions & 2 deletions helm-chart/dash0-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ The Dash0 operator is currently available as a technical preview.

Supported runtimes for automatic workload instrumentation:

* Node.js 18 and beyond
* Java 8+
* Node.js 18+

Other features like metrics and log collection are independent of the runtime of workloads.

Expand Down Expand Up @@ -878,7 +879,7 @@ Prometheus Rule Synchronization Results:
## Notes on Running The Operator on Apple Silicon

When running the operator on an Apple Silicon host (M1, M3 etc.), for example via Docker Desktop, some attention needs
to be paid to the CPU architecture of images. The architcture of the Kubernetes node for this scenario will be `arm64`.
to be paid to the CPU architecture of images. The architecture of the Kubernetes node for this scenario will be `arm64`.
When running a single-architecture `amd64` image (as opposed to a single-architecture `arm64` image or a
[multi-platform build](https://docs.docker.com/build/building/multi-platform/) containing `amd64` as well as `arm64`)
the operator will prevent the container from starting.
Expand Down
11 changes: 10 additions & 1 deletion images/instrumentation/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@ RUN gcc \
src/dash0_injector.c \
-o dash0_injector.so

# download OpenTelemetry Java agent
FROM openjdk:24-jdk-bookworm AS build-jvm
COPY jvm /dash0-init-container/instrumentation/jvm
WORKDIR /dash0-init-container/instrumentation/jvm
RUN ./mvnw dependency:copy-dependencies \
&& cp ./target/dependency/opentelemetry-javaagent-*.jar ./build/opentelemetry-javaagent.jar \
&& cp pom.xml ./build/pom.xml

# build Node.js artifacts
FROM node:20.13.1-alpine3.19 AS build-node.js
RUN mkdir -p /instrumentation/node.js
RUN mkdir -p /dash0-init-container/instrumentation/node.js
WORKDIR /dash0-init-container/instrumentation/node.js
COPY node.js/package* ./
COPY node.js/dash0hq-opentelemetry-*.tgz .
Expand All @@ -36,6 +44,7 @@ COPY copy-instrumentation.sh /
# copy artifacts (distros, injector binary) from the build stages to the final image
RUN mkdir -p /dash0-init-container/instrumentation
COPY --from=build-injector /dash0-init-container/dash0_injector.so /dash0-init-container/dash0_injector.so
COPY --from=build-jvm /dash0-init-container/instrumentation/jvm/build /dash0-init-container/instrumentation/jvm
COPY --from=build-node.js /dash0-init-container/instrumentation/node.js /dash0-init-container/instrumentation/node.js

WORKDIR /
Expand Down
184 changes: 117 additions & 67 deletions images/instrumentation/injector/src/dash0_injector.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#define HIGHS (ONES * (UCHAR_MAX / 2 + 1))
#define HASZERO(x) ((x) - ONES & ~(x) & HIGHS)

#define JAVA_TOOL_OPTIONS_ENV_VAR_NAME "JAVA_TOOL_OPTIONS"
#define JAVA_TOOL_OPTIONS_DASH0_REQUIRE \
"-javaagent:" \
"/__dash0__/instrumentation/jvm/opentelemetry-javaagent.jar"
#define NODE_OPTIONS_ENV_VAR_NAME "NODE_OPTIONS"
#define NODE_OPTIONS_DASH0_REQUIRE \
"--require " \
Expand Down Expand Up @@ -92,103 +96,149 @@ char *__getenv(const char *name) {
* the program manipulated values of env vars without dynamic allocations.
*/
char cachedModifiedOtelResourceAttributesValue[1012];
char cachedModifiedNodeOptionsValue[1012];
char cachedModifiedRuntimeOptionsValue[1012];

char *__appendResourceAttributes(const char *buffer, const char *origValue) {
char *namespaceName = __getenv(DASH0_NAMESPACE_NAME_ENV_VAR_NAME);
char *podUid = __getenv(DASH0_POD_UID_ENV_VAR_NAME);
char *podName = __getenv(DASH0_POD_NAME_ENV_VAR_NAME);
char *containerName = __getenv(DASH0_POD_CONTAINER_NAME_VAR_NAME);

int attributeCount = 0;

/*
* We do not perform octect escaping in the resource attributes as
* specified in
* https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable
* because the values that are passed down to the injector comes from
* fields that Kubernetes already enforces to either conform to RFC 1035
* or RFC RFC 1123
* (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names),
* and in either case, none of the characters allowed require escaping
* based on https://www.w3.org/TR/baggage/#header-content
*/

if (namespaceName != NULL && __strlen(namespaceName) > 0) {
__strcat(buffer, "k8s.namespace.name=");
__strcat(buffer, namespaceName);
attributeCount += 1;
}

if (podName != NULL && __strlen(podName) > 0) {
if (attributeCount > 0) {
__strcat(buffer, ",");
}

__strcat(buffer, "k8s.pod.name=");
__strcat(buffer, podName);
attributeCount += 1;
}

if (podUid != NULL && __strlen(podUid) > 0) {
if (attributeCount > 0) {
__strcat(buffer, ",");
}

__strcat(buffer, "k8s.pod.uid=");
__strcat(buffer, podUid);
attributeCount += 1;
}

if (containerName != NULL && __strlen(containerName) > 0) {
if (attributeCount > 0) {
__strcat(buffer, ",");
}

__strcat(buffer, "k8s.container.name=");
__strcat(buffer, containerName);
attributeCount += 1;
}

if (origValue != NULL && __strlen(origValue) > 0) {
if (attributeCount > 0) {
__strcat(buffer, ",");
}

__strcat(buffer, origValue);
}
}

char *getenv(const char *name) {
char *origValue = __getenv(name);
int l = __strlen(name);

char *otelResourceAttributesVarName = OTEL_RESOURCE_ATTRIBUTES_ENV_VAR_NAME;
char *javaToolOptionsVarName = JAVA_TOOL_OPTIONS_ENV_VAR_NAME;
char *nodeOptionsVarName = NODE_OPTIONS_ENV_VAR_NAME;
if (__strcmp(name, otelResourceAttributesVarName) == 0) {
if (__strlen(cachedModifiedOtelResourceAttributesValue) == 0) {
// This environment variable (OTEL_RESOURCE_ATTRIBUTES) has not been
// requested before, calculate the modified value and cache it.
char *namespaceName = __getenv(DASH0_NAMESPACE_NAME_ENV_VAR_NAME);
char *podUid = __getenv(DASH0_POD_UID_ENV_VAR_NAME);
char *podName = __getenv(DASH0_POD_NAME_ENV_VAR_NAME);
char *containerName = __getenv(DASH0_POD_CONTAINER_NAME_VAR_NAME);

int attributeCount = 0;

/*
* We do not perform octect escaping in the resource attributes as
* specified in
* https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable
* because the values that are passed down to the injector comes from
* fields that Kubernetes already enforces to either conform to RFC 1035
* or RFC RFC 1123
* (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names),
* and in either case, none of the characters allowed require escaping
* based on https://www.w3.org/TR/baggage/#header-content
*/

if (namespaceName != NULL && __strlen(namespaceName) > 0) {
__strcat(cachedModifiedOtelResourceAttributesValue,
"k8s.namespace.name=");
__strcat(cachedModifiedOtelResourceAttributesValue, namespaceName);
attributeCount += 1;
}

if (podName != NULL && __strlen(podName) > 0) {
if (attributeCount > 0) {
__strcat(cachedModifiedOtelResourceAttributesValue, ",");
}

__strcat(cachedModifiedOtelResourceAttributesValue, "k8s.pod.name=");
__strcat(cachedModifiedOtelResourceAttributesValue, podName);
attributeCount += 1;
}

if (podUid != NULL && __strlen(podUid) > 0) {
if (attributeCount > 0) {
__strcat(cachedModifiedOtelResourceAttributesValue, ",");
}

__strcat(cachedModifiedOtelResourceAttributesValue, "k8s.pod.uid=");
__strcat(cachedModifiedOtelResourceAttributesValue, podUid);
attributeCount += 1;
}
__appendResourceAttributes(cachedModifiedOtelResourceAttributesValue,
origValue);
}

if (containerName != NULL && __strlen(containerName) > 0) {
if (attributeCount > 0) {
__strcat(cachedModifiedOtelResourceAttributesValue, ",");
}
return cachedModifiedOtelResourceAttributesValue;
} else if (__strcmp(name, javaToolOptionsVarName) == 0) {
if (__strlen(cachedModifiedRuntimeOptionsValue) == 0) {
// No runtime environment variable has been requested before,
// calculate the modified value and cache it.

__strcat(cachedModifiedOtelResourceAttributesValue,
"k8s.container.name=");
__strcat(cachedModifiedOtelResourceAttributesValue, containerName);
attributeCount += 1;
}
// Prepend our --require as the first item to the JAVA_TOOL_OPTIONS
// string.
char *javaToolOptionsDash0Require = JAVA_TOOL_OPTIONS_DASH0_REQUIRE;
__strcat(cachedModifiedRuntimeOptionsValue, javaToolOptionsDash0Require);

// The Java runtime does not look up the OTEL_RESOURCE_ATTRIBUTES env var
// using getenv(), but rather by parsing the environment block
// (/proc/env/<pid>) directly, which we cannot affect with the getenv
// hook. So, instead, we append the resource attributes as the
// -Dotel.resource.attributes Java system property.
// If the -Dotel.resource.attributes system property is already set,
// the user-defined property will take precedence:
//
// % JAVA_TOOL_OPTIONS="-Dprop=B" jshell -R -Dprop=A
// Picked up JAVA_TOOL_OPTIONS: -Dprop=B
// | Welcome to JShell -- Version 17.0.12
// | For an introduction type: /help intro
//
// jshell> System.getProperty("prop")
// $1 ==> "A"

char *otelResourceAttributesViaEnv =
__getenv(OTEL_RESOURCE_ATTRIBUTES_ENV_VAR_NAME);
__strcat(cachedModifiedRuntimeOptionsValue,
" -Dotel.resource.attributes=");
__appendResourceAttributes(cachedModifiedRuntimeOptionsValue,
otelResourceAttributesViaEnv);

if (origValue != NULL && __strlen(origValue) > 0) {
if (attributeCount > 0) {
__strcat(cachedModifiedOtelResourceAttributesValue, ",");
}

__strcat(cachedModifiedOtelResourceAttributesValue, origValue);
// If JAVA_TOOL_OPTIONS were present, append the existing
// JAVA_TOOL_OPTIONS after our --javaagent.
__strcat(cachedModifiedRuntimeOptionsValue, " ");
__strcat(cachedModifiedRuntimeOptionsValue, origValue);
}
}

return cachedModifiedOtelResourceAttributesValue;
return cachedModifiedRuntimeOptionsValue;
} else if (__strcmp(name, nodeOptionsVarName) == 0) {
if (__strlen(cachedModifiedNodeOptionsValue) == 0) {
// This environment variable (NODE_OPTIONS) has not been requested before,
if (__strlen(cachedModifiedRuntimeOptionsValue) == 0) {
// No runtime environment variable has been requested before,
// calculate the modified value and cache it.

// Prepend our --require as the first item to the NODE_OPTIONS string.
char *nodeOptionsDash0Require = NODE_OPTIONS_DASH0_REQUIRE;
__strcat(cachedModifiedNodeOptionsValue, nodeOptionsDash0Require);
__strcat(cachedModifiedRuntimeOptionsValue, nodeOptionsDash0Require);

if (origValue != NULL && __strlen(origValue) > 0) {
// If NODE_OPTIONS were present, append the existing NODE_OPTIONS after
// our --require.
__strcat(cachedModifiedNodeOptionsValue, " ");
__strcat(cachedModifiedNodeOptionsValue, origValue);
__strcat(cachedModifiedRuntimeOptionsValue, " ");
__strcat(cachedModifiedRuntimeOptionsValue, origValue);
}
}

return cachedModifiedNodeOptionsValue;
return cachedModifiedRuntimeOptionsValue;
}

return origValue;
Expand Down
14 changes: 14 additions & 0 deletions images/instrumentation/jvm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Download
build/*

# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
19 changes: 19 additions & 0 deletions images/instrumentation/jvm/.mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
10 changes: 10 additions & 0 deletions images/instrumentation/jvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# JVM instrumentation

We currently do not have a Dash0 distro for Java.
Rather, we use the upstream [OpenTelemetry Java agent](https://github.com/open-telemetry/opentelemetry-java-instrumentation).
The version of the OTel Java agent to be used is specified in the [local `pom.xml`](./pom.xml) file.
Maven can download the correct version of the OTel Java agent by running:

```shell
./mvnw dependency:copy-dependencies
```
Empty file.
Loading

0 comments on commit 088b15c

Please sign in to comment.