diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a0d3cedc..c7d4c14c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,7 +29,7 @@ concurrency: jobs: verify: name: Build & Test - runs-on: ubuntu-latest + runs-on: 8core_32gb timeout-minutes: 15 steps: @@ -76,7 +76,15 @@ jobs: injector_binary_and_instrumentation_image_tests: name: Injector Binary & Instrumentation Image Tests - runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + # We run the job on separate architectures to avoid using emulation in Docker + # for transpiling (which is somehow broken in Zig with QEmu below 9, see + # https://github.com/ziglang/zig/issues/22643), and SLOW. + runner: ["8core_32gb", "8core_32gb_arm64"] + runs-on: ${{ matrix.runner }} timeout-minutes: 40 steps: - uses: actions/checkout@v4 @@ -110,6 +118,16 @@ jobs: - .github/workflows/ci.yaml - images/instrumentation/** + - name: builder arch to docker + id: docker-arch + run: | + if [ $(arch) = 'aarch64' ] + then + echo 'docker_arch=linux/arm64' >> $GITHUB_OUTPUT + else + echo 'docker_arch=linux/amd64' >> $GITHUB_OUTPUT + fi + - name: show changed files env: INSTRUMENTATION_CHANGED_FILES_FLAG: ${{ steps.changed-files.outputs.instrumentation_any_changed }} @@ -124,14 +142,6 @@ jobs: steps.changed-files.outputs.instrumentation_any_changed == 'true' || github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') - # Just for building on arm, buildx is enough but doing docker run with --platform=linux/arm64 (which we do when - # testing the injector binary and the instrumentation image) requires qemu. - - name: set up qemu - uses: docker/setup-qemu-action@v3 - if: | - steps.changed-files.outputs.instrumentation_any_changed == 'true' || - github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') - - name: login to GitHub container registry uses: docker/login-action@v3 if: | @@ -153,6 +163,7 @@ jobs: echo "image_name=ghcr.io/dash0hq/instrumentation-ci-test:$image_tag" >> $GITHUB_OUTPUT - name: build temporary instrumentation image + id: build-test-instrumentation-image uses: docker/build-push-action@v6 # Dependabot PRs currently fail in this step, see # https://stackoverflow.com/questions/74788092/github-dependabot-doesnt-have-permissions-to-publish-to-ghcr-how-can-i-give-it @@ -169,26 +180,19 @@ jobs: with: context: images/instrumentation tags: ${{ steps.instrumentation-test-image.outputs.image_name }} - platforms: linux/amd64,linux/arm64 + platforms: ${{ steps.docker-arch.outputs.docker_arch }} cache-from: type=gha,scope=instrumentation cache-to: type=gha,mode=max,scope=instrumentation push: true - - name: injector tests - if: | - steps.changed-files.outputs.instrumentation_any_changed == 'true' || - github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') - env: - INSTRUMENTATION_IMAGE: ${{ steps.instrumentation-test-image.outputs.image_name }} - run: | - images/instrumentation/injector/test/scripts/test-all.sh - - name: instrumentation image tests if: | steps.changed-files.outputs.instrumentation_any_changed == 'true' || github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') env: - INSTRUMENTATION_IMAGE: ${{ steps.instrumentation-test-image.outputs.image_name }} + INSTRUMENTATION_IMAGE: ${{ steps.instrumentation-test-image.outputs.image_name }}@${{ steps.build-test-instrumentation-image.outputs.imageid }} + IS_REMOTE_IMAGE: false + ARCHITECTURES: ${{ steps.docker-arch.outputs.docker_arch }} run: | images/instrumentation/test/test-all.sh @@ -210,7 +214,7 @@ jobs: # meant for production use. build-and-push-images: name: Build Images - runs-on: ubuntu-latest + runs-on: 8core_32gb_arm64 needs: - verify - injector_binary_and_instrumentation_image_tests @@ -275,7 +279,7 @@ jobs: publish-helm-chart-dry-run: name: Publish Helm Chart (Dry Run) - runs-on: ubuntu-latest + runs-on: 8core_32gb_arm64 if: ${{ ! contains(github.ref, 'refs/tags/') && github.actor != 'dependabot[bot]'}} needs: - build-and-push-images @@ -297,7 +301,8 @@ jobs: # branch protection rules reference this property, and it is a required check. skip-publish-helm-chart-dry-run-for-dependabot: name: Publish Helm Chart (Dry Run) - runs-on: ubuntu-latest + runs-on: 8core_32gb_arm64 + if: ${{ ! contains(github.ref, 'refs/tags/') && github.actor == 'dependabot[bot]'}} needs: - build-and-push-images @@ -309,7 +314,7 @@ jobs: publish-helm-chart: name: Publish Helm Chart - runs-on: ubuntu-latest + runs-on: 8core_32gb_arm64 if: ${{ contains(github.ref, 'refs/tags/') && github.actor != 'dependabot[bot]'}} needs: - build-and-push-images diff --git a/images/instrumentation/Dockerfile b/images/instrumentation/Dockerfile index 4659b4bc..4508c6de 100644 --- a/images/instrumentation/Dockerfile +++ b/images/instrumentation/Dockerfile @@ -1,22 +1,15 @@ # injector_build_start - do not remove this line (see images/instrumentation/injector/test/scripts/test-all.sh) # build injector -FROM ubuntu:24.04 AS build-injector +FROM alpine:3.21 AS build-injector -RUN apt-get update && \ - apt-get install --no-install-recommends build-essential -y && \ - apt-get autoremove -y && \ - apt-get clean -y +## Zig is available as a community package on Alpine +RUN apk add zig --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community COPY ./injector /dash0-init-container +ARG TARGETARCH WORKDIR /dash0-init-container -RUN gcc \ - -shared \ - -nostdlib \ - -fPIC \ - -Wl,--version-script=src/dash0_injector.exports.map \ - src/dash0_injector.c \ - -o dash0_injector.so +RUN zig build -Dcpu-arch=${TARGETARCH} --summary all # injector_build_end - do not remove this line (see images/instrumentation/injector/test/scripts/test-all.sh) @@ -42,12 +35,12 @@ RUN NPM_CONFIG_UPDATE_NOTIFIER=false \ --no-fund=true # build final image -FROM alpine:3.19.1 +FROM alpine:3.21 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-injector /dash0-init-container/bin /dash0-init-container 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 diff --git a/images/instrumentation/injector/.gitignore b/images/instrumentation/injector/.gitignore new file mode 100644 index 00000000..7c046b64 --- /dev/null +++ b/images/instrumentation/injector/.gitignore @@ -0,0 +1,2 @@ +.zig-cache +zig-out \ No newline at end of file diff --git a/images/instrumentation/injector/README.md b/images/instrumentation/injector/README.md new file mode 100644 index 00000000..e0d3c9ce --- /dev/null +++ b/images/instrumentation/injector/README.md @@ -0,0 +1,5 @@ + + + + +https://richiejp.com/zig-ld-preload-trick \ No newline at end of file diff --git a/images/instrumentation/injector/bin/NOTICE b/images/instrumentation/injector/bin/NOTICE index 350e7a69..bb237899 100644 --- a/images/instrumentation/injector/bin/NOTICE +++ b/images/instrumentation/injector/bin/NOTICE @@ -10,32 +10,4 @@ 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. - - - - -This codebase contains code from musl libc (https://musl.libc.org) -subject to the following license terms: - ----------------------------------------------------------------------- -Copyright © 2005-2014 Rich Felker, et al. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +limitations under the License. \ No newline at end of file diff --git a/images/instrumentation/injector/build.zig b/images/instrumentation/injector/build.zig new file mode 100644 index 00000000..a0426916 --- /dev/null +++ b/images/instrumentation/injector/build.zig @@ -0,0 +1,60 @@ +const std = @import("std"); + +pub const InjectorBuildError = error{UnsupportedArchitecturError}; + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. + +pub fn build(b: *std.Build) !void { + const optimize = b.standardOptimizeOption(.{}); + + var targetCpuArch = std.Target.Cpu.Arch.aarch64; + var targetCpuModel = std.Target.Cpu.Model.generic(std.Target.Cpu.Arch.aarch64); + + if (b.option([]const u8, "cpu-arch", "The system architecture to compile the injector for; valid options are 'amd64' and 'arch64' (default)")) |val| { + if (std.mem.eql(u8, "arm64", val)) { + // Nothing to do + } else if (std.mem.eql(u8, "amd64", val)) { + targetCpuArch = std.Target.Cpu.Arch.x86_64; + targetCpuModel = std.Target.Cpu.Model.generic(std.Target.Cpu.Arch.x86_64); + } else { + return error.UnsupportedArchitecturError; + } + } + + const target = b.resolveTargetQuery(.{ + .cpu_arch = targetCpuArch, + // Skip cpu model detection because the automatic detection for transpiling fails in build + .cpu_model = .{ .explicit = targetCpuModel }, + .os_tag = .linux, + }); + + const lib = b.addSharedLibrary(.{ + .name = "injector", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/dash0_injector.zig"), + .target = target, + .optimize = optimize, + .link_libc = false, + .pic = true, + .strip = false, + }); + lib.setVersionScript(b.path("src/dash0_injector.exports.map")); + + b.getInstallStep().dependOn(&b.addInstallArtifact(lib, .{ .dest_dir = .{ .override = .{ .custom = "." } } }).step); + + var copy_injector_to_bin = b.step("copy_file", "Copy injector file"); + copy_injector_to_bin.makeFn = copyInjectorFile; + copy_injector_to_bin.dependOn(b.getInstallStep()); + + b.default_step = copy_injector_to_bin; +} + +fn copyInjectorFile(step: *std.Build.Step, _: std.Progress.Node) anyerror!void { + const source_path = step.owner.pathFromRoot("./zig-out/libinjector.so"); + const dest_path = step.owner.pathFromRoot("./bin/dash0_injector.so"); + + try std.fs.copyFileAbsolute(source_path, dest_path, .{}); +} diff --git a/images/instrumentation/injector/build.zig.zon b/images/instrumentation/injector/build.zig.zon new file mode 100644 index 00000000..776d9eef --- /dev/null +++ b/images/instrumentation/injector/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "injector", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/images/instrumentation/injector/src/dash0_injector.c b/images/instrumentation/injector/src/dash0_injector.c deleted file mode 100644 index c1c27599..00000000 --- a/images/instrumentation/injector/src/dash0_injector.c +++ /dev/null @@ -1,245 +0,0 @@ -#include -#include - -#define ALIGN (sizeof(size_t)) -#define UCHAR_MAX 255 -#define ONES ((size_t)-1 / UCHAR_MAX) -#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 " \ - "/__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" -#define OTEL_RESOURCE_ATTRIBUTES_ENV_VAR_NAME "OTEL_RESOURCE_ATTRIBUTES" -#define DASH0_NAMESPACE_NAME_ENV_VAR_NAME "DASH0_NAMESPACE_NAME" -#define DASH0_POD_UID_ENV_VAR_NAME "DASH0_POD_UID" -#define DASH0_POD_NAME_ENV_VAR_NAME "DASH0_POD_NAME" -#define DASH0_POD_CONTAINER_NAME_VAR_NAME "DASH0_CONTAINER_NAME" - -extern char **__environ; - -size_t __strlen(const char *s) { - const char *a = s; - const size_t *w; - for (; (uintptr_t)s % ALIGN; s++) - if (!*s) - return s - a; - for (w = (const void *)s; !HASZERO(*w); w++) - ; - for (s = (const void *)w; *s; s++) - ; - return s - a; -} - -char *__strchrnul(const char *s, int c) { - size_t *w, k; - - c = (unsigned char)c; - if (!c) - return (char *)s + __strlen(s); - - for (; (uintptr_t)s % ALIGN; s++) - if (!*s || *(unsigned char *)s == c) - return (char *)s; - k = ONES * c; - for (w = (void *)s; !HASZERO(*w) && !HASZERO(*w ^ k); w++) - ; - for (s = (void *)w; *s && *(unsigned char *)s != c; s++) - ; - return (char *)s; -} - -char *__strcpy(char *restrict dest, const char *restrict src) { - const unsigned char *s = src; - unsigned char *d = dest; - while ((*d++ = *s++)) - ; - return dest; -} - -char *__strcat(char *restrict dest, const char *restrict src) { - __strcpy(dest + __strlen(dest), src); - return dest; -} - -int __strcmp(const char *l, const char *r) { - for (; *l == *r && *l; l++, r++) - ; - return *(unsigned char *)l - *(unsigned char *)r; -} - -int __strncmp(const char *_l, const char *_r, size_t n) { - const unsigned char *l = (void *)_l, *r = (void *)_r; - if (!n--) - return 0; - for (; *l && *r && n && *l == *r; l++, r++, n--) - ; - return *l - *r; -} - -char *__getenv(const char *name) { - size_t l = __strchrnul(name, '=') - name; - if (l && !name[l] && __environ) - for (char **e = __environ; *e; e++) - if (!__strncmp(name, *e, l) && l[*e] == '=') - return *e + l + 1; - return 0; -} - -/* - * Buffers of statically-allocated memory that we can use to safely return to - * the program manipulated values of env vars without dynamic allocations. - */ -char cachedModifiedOtelResourceAttributesValue[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. - __appendResourceAttributes(cachedModifiedOtelResourceAttributesValue, - origValue); - } - - 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. - - // 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/) 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 JAVA_TOOL_OPTIONS were present, append the existing - // JAVA_TOOL_OPTIONS after our --javaagent. - __strcat(cachedModifiedRuntimeOptionsValue, " "); - __strcat(cachedModifiedRuntimeOptionsValue, origValue); - } - } - - return cachedModifiedRuntimeOptionsValue; - } else if (__strcmp(name, nodeOptionsVarName) == 0) { - 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(cachedModifiedRuntimeOptionsValue, nodeOptionsDash0Require); - - if (origValue != NULL && __strlen(origValue) > 0) { - // If NODE_OPTIONS were present, append the existing NODE_OPTIONS after - // our --require. - __strcat(cachedModifiedRuntimeOptionsValue, " "); - __strcat(cachedModifiedRuntimeOptionsValue, origValue); - } - } - - return cachedModifiedRuntimeOptionsValue; - } - - return origValue; -} diff --git a/images/instrumentation/injector/src/dash0_injector.zig b/images/instrumentation/injector/src/dash0_injector.zig new file mode 100644 index 00000000..4960c7dc --- /dev/null +++ b/images/instrumentation/injector/src/dash0_injector.zig @@ -0,0 +1,303 @@ +const std = @import("std"); + +const null_terminated_string = [*:0]const u8; + +const dash0_log_prefix = "Dash0 injector: "; + +const dash0_java_tool_options_addition = "-javaagent:/__dash0__/instrumentation/jvm/opentelemetry-javaagent.jar"; +const dash0_node_options_addition = "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry"; + +const _envMutex = std.Thread.Mutex{}; + +export fn getenv(nameZ: null_terminated_string) ?null_terminated_string { + const name = std.mem.sliceTo(nameZ, 0); + + // Some runtimes like Node.js look uop variables in a multithreaded fashion + var envMutex = _envMutex; + envMutex.lock(); + defer envMutex.unlock(); + + const allocator = getAllocator(); + const envMap = getEnvMap(allocator) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot retrieve process environment, returning null for environment variable '{s}': {}\n", .{ name, err }); + return null; + }; + + return getEnvValue(allocator, envMap, name); +} + +fn getEnvValue(allocator: std.mem.Allocator, envMap: std.process.EnvMap, name: [:0]const u8) ?null_terminated_string { + const value = envMap.get(name); + + if (std.mem.eql(u8, name, "OTEL_RESOURCE_ATTRIBUTES")) { + const resource_attributes = getResourceAttributes(allocator, envMap) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot look up resource attributes from environment to calculate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + if (resource_attributes.len < 1) { + // No resource attributes to add. Return a pointer to the current value, + // or null if there is no current value. + if (value) |val| { + const return_buffer = std.fmt.allocPrintZ(allocator, "{s}", .{val}) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } else { + return null; + } + } + + if (value) |val| { + const return_buffer = std.fmt.allocPrintZ(allocator, "{s},{s}", .{ resource_attributes, val }) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } + + const return_buffer = std.fmt.allocPrintZ(allocator, "{s}", .{resource_attributes}) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } else if (std.mem.eql(u8, name, "JAVA_TOOL_OPTIONS")) { + // The Java runtime does not look up the OTEL_RESOURCE_ATTRIBUTES env var + // using getenv(), but rather by parsing the environment block + // (/proc/env/) 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" + if (value) |val| { + const resourceAttributes = getResourceAttributes(allocator, envMap) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot retrieve resource attributes: {}\n", .{err}); + return ""; + }; + + if (resourceAttributes.len > 0) { + const return_buffer = std.fmt.allocPrintZ(allocator, "{s} {s} -Dotel.resource.attributes={s}", .{ val, dash0_java_tool_options_addition, resourceAttributes }) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } else { + // If JAVA_TOOL_OPTIONS were present, append the existing JAVA_TOOL_OPTIONS after our --javaagent. + const return_buffer = std.fmt.allocPrintZ(allocator, "{s} {s}", .{ dash0_java_tool_options_addition, val }) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } + } else { + return dash0_java_tool_options_addition[0..].ptr; + } + } else if (std.mem.eql(u8, name, "NODE_OPTIONS")) { + if (value) |val| { + // If NODE_OPTIONS were present, append the existing NODE_OPTIONS after our --require. + const return_buffer = std.fmt.allocPrintZ(allocator, "{s} {s}", .{ dash0_node_options_addition, val }) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to manipulate the value of '{s}': {}\n", .{ name, err }); + return null; + }; + + return return_buffer.ptr; + } else { + return dash0_node_options_addition[0..].ptr; + } + } + + if (value) |val| { + if (val.len > 0) { + const return_buffer = std.fmt.allocPrintZ(allocator, "{s}", .{val}) catch |err| { + std.debug.print(dash0_log_prefix ++ "Cannot allocate memory to return the unmodified value '{s}' of '{s}': {}\n", .{ val, name, err }); + return null; + }; + + return return_buffer.ptr; + } + } + + return null; +} + +// In Linux 2.6.13+ there is the rlimit_resource, but we cannot access it at compile time +// and assume it is the same for whatever Linux the injector will run on. +// Instead, we assume the 128KB limit of Linux before 2.6.13, and align it with the memory page +// size of 4KB that the GeneralPurposeAllocator is anyhow going to get. +// Note: Linux mem pages are not always 4KB: https://github.com/ziglang/zig/issues/11308 +const memory_page_size = 4096; +const default_memory_limit = 32 * memory_page_size; +var gpa = std.heap.GeneralPurposeAllocator(.{ .enable_memory_limit = true }){}; +var _allocator: ?std.mem.Allocator = null; + +fn getAllocator() std.mem.Allocator { + if (_allocator) |allocator| { + return allocator; + } + + gpa.requested_memory_limit = getAllocatorMemoryLimit(); + const allocator = gpa.allocator(); + _allocator = allocator; + return allocator; +} + +fn getAllocatorMemoryLimit() usize { + return getEnvironBufferSize() catch |err| { + std.debug.print(dash0_log_prefix ++ "Using default memory limit of {} bytes because an error occurred while retrieving the process environment size: {}\n", .{ default_memory_limit, err }); + return default_memory_limit; + }; +} + +fn getEnvironBufferSize() !usize { + var file = try std.fs.cwd().openFile("/proc/self/environ", .{ .mode = .read_only }); + defer file.close(); + + // Cannot use file.stat(), it returns zero because we are reading a virtual file + var buf_reader = std.io.bufferedReader(file.reader()); + var in_stream = buf_reader.reader(); + + var size: usize = 0; + while (true) { + _ = in_stream.readByte() catch |err| { + if (err == error.EndOfStream) { + break; + } + + return err; + }; + size += 1; + } + + var res: usize = 0; + while (size > res) { + // We could use casting and other dangerous stuff and be a little faster. + // Or we could do this one-time thing in the dumb and safe way. + res += memory_page_size; + } + + // Add another memory page for good measure + return res + memory_page_size; +} + +const EnvToResourceAttributeMapping = struct { + environement_variable_name: []const u8, + resource_attributes_key: []const u8, +}; + +const mappings: [4]EnvToResourceAttributeMapping = .{ EnvToResourceAttributeMapping{ + .environement_variable_name = "DASH0_NAMESPACE_NAME", + .resource_attributes_key = "k8s.namespace.name", +}, EnvToResourceAttributeMapping{ + .environement_variable_name = "DASH0_POD_NAME", + .resource_attributes_key = "k8s.pod.name", +}, EnvToResourceAttributeMapping{ + .environement_variable_name = "DASH0_POD_UID", + .resource_attributes_key = "k8s.pod.uid", +}, EnvToResourceAttributeMapping{ + .environement_variable_name = "DASH0_CONTAINER_NAME", + .resource_attributes_key = "k8s.container.name", +} }; + +fn getResourceAttributes(allocator: std.mem.Allocator, envMap: std.process.EnvMap) ![]u8 { + var final_len: usize = 0; + + for (mappings) |mapping| { + if (envMap.get(mapping.environement_variable_name)) |value| { + if (final_len > 0) { + final_len += 1; // ";" + } + + final_len += mapping.resource_attributes_key.len; + final_len += 1; // "=" + final_len += value.len; + } + } + + var resourceAttributes = try allocator.alloc(u8, final_len); + // Do not defer, we need to return this memory location outside of the injector + + var i: usize = 0; + for (mappings) |mapping| { + if (i > 0) { + resourceAttributes[i] = ','; + i += 1; + } + + if (envMap.get(mapping.environement_variable_name)) |value| { + for (mapping.resource_attributes_key) |char| { + resourceAttributes[i] = char; + i += 1; + } + + resourceAttributes[i] = '='; + i += 1; + + for (value) |char| { + resourceAttributes[i] = char; + i += 1; + } + } + } + + // Return slice, buffer has deferred free + return resourceAttributes; +} + +const EnvironmentMapCreationError = error{ + CannotOpenProcessEnviron, + CannotReadProcessEnviron, +}; + +var _environ: ?std.process.EnvMap = null; + +fn getEnvMap(allocator: std.mem.Allocator) !std.process.EnvMap { + if (_environ) |envMap| { + return envMap; + } + + var file = try std.fs.cwd().openFile("/proc/self/environ", .{ .mode = .read_only }); + defer file.close(); + + const in_stream = file.reader(); + const max_size = std.math.maxInt(u32); // TODO Find better limit + const environ_bytes = try in_stream.readAllAlloc(allocator, max_size); + + var res = std.process.EnvMap.init(allocator); + + var i: usize = 0; + + while (environ_bytes.len > i) { + const env_var: []u8 = std.mem.sliceTo(environ_bytes[i..], 0); + + if (std.mem.indexOf(u8, env_var, "=")) |index_first_equals| { + const key = env_var[0..index_first_equals]; + const value = env_var[index_first_equals + 1 ..]; + + res.put(key, value) catch |err| { + // TODO Can we add args to the context? + return err; + }; + + i += env_var.len + 1; + } else { + break; + } + } + + _environ = res; + return res; +} diff --git a/images/instrumentation/injector/test/README.md b/images/instrumentation/injector/test/README.md deleted file mode 100644 index 82bf3541..00000000 --- a/images/instrumentation/injector/test/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This directory contains isolated tests for the injector code. -The difference to images/instrumentation/test is that the latter tests the whole instrumentation image. -Also, the tests in this folder do not use multi-platform images, an injector binary is build (in a container) per CPU -architecture, and then used for testing. diff --git a/images/instrumentation/injector/test/app/index.js b/images/instrumentation/injector/test/app/index.js deleted file mode 100644 index 235bcd3c..00000000 --- a/images/instrumentation/injector/test/app/index.js +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -// SPDX-License-Identifier: Apache-2.0 - -const process = require('node:process'); - -function echoEnvVar(envVarName) { - const envVarValue = process.env[envVarName]; - if (!envVarValue) { - process.stdout.write(`${envVarName}: -`); - } else { - process.stdout.write(`${envVarName}: ${envVarValue}`); - } -} - -function main () { - const testCase = process.argv[2]; - if (!testCase) { - console.error("error: not enough arguments, the name of the test case needs to be specifed"); - process.exit(1) - } - - switch (testCase) { - case "non-existing": - echoEnvVar("DOES_NOT_EXIST"); - break; - case "existing": - echoEnvVar("TEST_VAR"); - break; - case "node_options": - echoEnvVar("NODE_OPTIONS"); - break; - case "node_options_twice": - echoEnvVar("NODE_OPTIONS"); - process.stdout.write("; ") - echoEnvVar("NODE_OPTIONS"); - break; - case "otel_resource_attributes": - echoEnvVar("OTEL_RESOURCE_ATTRIBUTES"); - break; - default: - console.error(`unknown test case: ${testCase}`); - process.exit(1) - } -} - -main(); diff --git a/images/instrumentation/injector/test/bin/.gitignore b/images/instrumentation/injector/test/bin/.gitignore deleted file mode 100644 index 140f8cf8..00000000 --- a/images/instrumentation/injector/test/bin/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.so diff --git a/images/instrumentation/injector/test/docker/.gitignore b/images/instrumentation/injector/test/docker/.gitignore deleted file mode 100644 index 234ed3e4..00000000 --- a/images/instrumentation/injector/test/docker/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Dockerfile-build diff --git a/images/instrumentation/injector/test/docker/Dockerfile-test b/images/instrumentation/injector/test/docker/Dockerfile-test deleted file mode 100644 index 1550ea3a..00000000 --- a/images/instrumentation/injector/test/docker/Dockerfile-test +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -ARG base_image="node:20.18-bookworm" -FROM ${base_image} - -ARG injector_binary=no_value -# fail build immediately if build arg injector_binary is missing -RUN test ${injector_binary} != "no_value" - -WORKDIR /usr/src/dash0/injector/ - -COPY app app -COPY scripts/run-tests-within-container.sh scripts/ - -RUN echo ${injector_binary} - -COPY bin/${injector_binary} /dash0-init-container/injector/dash0_injector.so - -COPY docker/noop.js /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry/index.js - -CMD ["scripts/run-tests-within-container.sh"] diff --git a/images/instrumentation/injector/test/docker/noop.js b/images/instrumentation/injector/test/docker/noop.js deleted file mode 100644 index 1fd0cd7e..00000000 --- a/images/instrumentation/injector/test/docker/noop.js +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -// SPDX-License-Identifier: Apache-2.0 - diff --git a/images/instrumentation/injector/test/scripts/build-in-container.sh b/images/instrumentation/injector/test/scripts/build-in-container.sh deleted file mode 100755 index b2ee007c..00000000 --- a/images/instrumentation/injector/test/scripts/build-in-container.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -set -eu - -cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. - -# shellcheck source=images/instrumentation/injector/test/scripts/util -source injector/test/scripts/util - -if [ -z "${ARCH:-}" ]; then - ARCH=arm64 -fi -if [ "$ARCH" = arm64 ]; then - docker_platform=linux/arm64 -elif [ "$ARCH" = x86_64 ]; then - docker_platform=linux/amd64 -else - echo "The architecture $ARCH is not supported." - exit 1 -fi - -dockerfile_name=injector/test/docker/Dockerfile-build -image_name=dash0-injector-builder-$ARCH -container_name=$image_name - -echo -echo -echo ">>> Building the library on $ARCH <<<" - -docker rmi -f "$image_name" 2> /dev/null -docker rm -f "$container_name" 2> /dev/null - -docker build \ - --platform "$docker_platform" \ - . \ - -f "$dockerfile_name" \ - -t "$image_name" - -copy_injector_binary_from_container_image "$image_name" "$ARCH" "$docker_platform" - diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh deleted file mode 100755 index 3c95f093..00000000 --- a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -set -eu - -cd "$(dirname "${BASH_SOURCE[0]}")"/.. - -if [ -z "${ARCH:-}" ]; then - ARCH=arm64 -fi -if [ "$ARCH" = arm64 ]; then - docker_platform=linux/arm64 - expected_cpu_architecture=aarch64 - injector_binary=dash0_injector_arm64.so -elif [ "$ARCH" = x86_64 ]; then - docker_platform=linux/amd64 - expected_cpu_architecture=x86_64 - injector_binary=dash0_injector_x86_64.so -else - echo "The architecture $ARCH is not supported." - exit 1 -fi - -if [ -z "${LIBC:-}" ]; then - LIBC=glibc -fi - -base_image=node:20.18-bookworm -if [[ "$LIBC" == "musl" ]]; then - base_image=node:20.18-alpine3.20 -fi - -dockerfile_name="docker/Dockerfile-test" -image_name=dash0-injector-test-$ARCH-$LIBC -container_name=$image_name - -docker_run_extra_arguments="" -if [ "${INTERACTIVE:-}" = "true" ]; then - if [ "$LIBC" = glibc ]; then - docker_run_extra_arguments=/bin/bash - elif [ "$LIBC" = musl ]; then - docker_run_extra_arguments=/bin/sh - else - echo "The libc flavor $LIBC is not supported." - exit 1 - fi -fi - -echo -echo ---------------------------------------- -echo "testing the injector library on $ARCH and $LIBC" -echo ---------------------------------------- - -docker rm -f "$container_name" 2> /dev/null -docker rmi -f "$image_name" 2> /dev/null - -set -x -docker build \ - --platform "$docker_platform" \ - --build-arg "base_image=${base_image}" \ - --build-arg "injector_binary=${injector_binary}" \ - . \ - -f "$dockerfile_name" \ - -t "$image_name" - -docker run \ - --platform "$docker_platform" \ - --env EXPECTED_CPU_ARCHITECTURE="$expected_cpu_architecture" \ - --name "$container_name" \ - "$image_name" \ - $docker_run_extra_arguments -{ set +x; } 2> /dev/null diff --git a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh deleted file mode 100755 index f185e03e..00000000 --- a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2059 - -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -set -eu - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -if [ -z "${EXPECTED_CPU_ARCHITECTURE:-}" ]; then - echo "EXPECTED_CPU_ARCHITECTURE is not set for $0." - exit 1 -fi - -arch=$(uname -m) -arch_exit_code=$? -if [ $arch_exit_code != 0 ]; then - printf "${RED}verifying CPU architecture failed:${NC}\n" - echo "exit code: $arch_exit_code" - echo "output: $arch" - exit 1 -elif [ "$arch" != "$EXPECTED_CPU_ARCHITECTURE" ]; then - printf "${RED}verifying CPU architecture failed:${NC}\n" - echo "expected: $EXPECTED_CPU_ARCHITECTURE" - echo "actual: $arch" - exit 1 -else - printf "${GREEN}verifying CPU architecture %s successful${NC}\n" "$EXPECTED_CPU_ARCHITECTURE" -fi - -injector_binary=/dash0-init-container/injector/dash0_injector.so -if [ ! -f $injector_binary ]; then - printf "${RED}error: %s does not exist, not running any tests.${NC}\n" "$injector_binary" - exit 1 -fi - -run_test_case() { - test_case_label=$1 - test_case_id=$2 - expected=$3 - existing_node_options_value=${4:-} - existing_otel_resource_attributes=${5:-} - full_command="LD_PRELOAD=""$injector_binary"" TEST_VAR=value DASH0_NAMESPACE_NAME=my-namespace DASH0_POD_NAME=my-pod DASH0_POD_UID=275ecb36-5aa8-4c2a-9c47-d8bb681b9aff DASH0_CONTAINER_NAME=test-app" - if [ "$existing_node_options_value" != "" ]; then - full_command=" $full_command NODE_OPTIONS=$existing_node_options_value" - fi - if [ "$existing_otel_resource_attributes" != "" ]; then - full_command=" $full_command OTEL_RESOURCE_ATTRIBUTES=$existing_otel_resource_attributes" - fi - full_command=" $full_command node index.js $test_case_id" - echo "Running test command: $full_command" - set +e - test_output=$(eval "$full_command") - test_exit_code=$? - set -e - if [ $test_exit_code != 0 ]; then - printf "${RED}test \"%s\" crashed:${NC}\n" "$test_case_label" - echo "received exit code: $test_exit_code" - echo "output: $test_output" - exit_code=1 - elif [ "$test_output" != "$expected" ]; then - printf "${RED}test \"%s\" failed:${NC}\n" "$test_case_label" - echo "expected: $expected" - echo "actual: $test_output" - exit_code=1 - else - printf "${GREEN}test \"%s\" successful${NC}\n" "$test_case_label" - fi -} - -exit_code=0 - -cd app - -run_test_case "getenv: returns undefined for non-existing environment variable" non-existing "DOES_NOT_EXIST: -" -run_test_case "getenv: returns environment variable unchanged" existing "TEST_VAR: value" -run_test_case "getenv: overrides NODE_OPTIONS if it is not present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" -run_test_case "getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" -run_test_case "getenv: prepends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation" "--no-deprecation" -run_test_case "getenv: ask for NODE_OPTIONS (set) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation" "--no-deprecation" -run_test_case "getenv: sets k8s.pod.uid and k8s.container.name via OTEL_RESOURCE_ATTRIBUTES" otel_resource_attributes "OTEL_RESOURCE_ATTRIBUTES: k8s.namespace.name=my-namespace,k8s.pod.name=my-pod,k8s.pod.uid=275ecb36-5aa8-4c2a-9c47-d8bb681b9aff,k8s.container.name=test-app" "" -run_test_case "getenv: sets k8s.pod.uid and k8s.container.name via OTEL_RESOURCE_ATTRIBUTES with pre-existing value" otel_resource_attributes "OTEL_RESOURCE_ATTRIBUTES: k8s.namespace.name=my-namespace,k8s.pod.name=my-pod,k8s.pod.uid=275ecb36-5aa8-4c2a-9c47-d8bb681b9aff,k8s.container.name=test-app,foo=bar" "" "foo=bar" - -exit $exit_code - diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh deleted file mode 100755 index bb8d0bbe..00000000 --- a/images/instrumentation/injector/test/scripts/test-all.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2059 - -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -set -eu - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' -dockerfile_injector_build=injector/test/docker/Dockerfile-build - -cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. - -# shellcheck source=images/instrumentation/injector/test/scripts/util -source injector/test/scripts/util - -# remove all outdated injector binaries -rm -rf injector/test/bin/* - -exit_code=0 -summary="" -run_tests_for_architecture_and_libc_flavor() { - arch=$1 - libc=$2 - set +e - ARCH=$arch LIBC=$libc injector/test/scripts/run-tests-for-container.sh - test_exit_code=$? - set -e - echo - echo ---------------------------------------- - if [ $test_exit_code != 0 ]; then - printf "${RED}tests for %s/%s failed (see above for details)${NC}\n" "$arch" "$libc" - exit_code=1 - summary="$summary\n$arch/$libc:\t${RED}failed${NC}" - else - printf "${GREEN}tests for %s/%s were successful${NC}\n" "$arch" "$libc" - summary="$summary\n$arch/$libc:\t${GREEN}ok${NC}" - fi - echo ---------------------------------------- - echo -} - -# Create a Dockerfile for building the injector in a container by re-using an excerpt from the ../Dockerfile. This makes -# sure we keep the way we build the injector binary here for this test suite and for actual production usage in the -# instrumentation image in sync. -copy_lines=false -rm -f "$dockerfile_injector_build" -while IFS= read -r line; do - if [[ "$line" =~ ^.*injector_build_start.*$ ]]; then - copy_lines=true - continue - fi - if [[ $copy_lines == true && "$line" =~ ^.*injector_build_end.*$ ]]; then - copy_lines=false - continue - fi - if [[ "$line" =~ ^[[:space:]]*#.* ]]; then - # skip comments - continue - fi - if [[ $copy_lines == true ]]; then - echo "$line" >> "$dockerfile_injector_build" - fi -done < Dockerfile - -if [[ ! -e "$dockerfile_injector_build" ]]; then - printf "\nError: The file $dockerfile_injector_build has not been generated, stopping." - exit 1 -fi - -instrumentation_image=${INSTRUMENTATION_IMAGE:-} -if [[ -z "$instrumentation_image" ]]; then - # build injector binary for both architectures - echo ---------------------------------------- - echo building the injector binary locally from source - echo ---------------------------------------- - ARCH=arm64 injector/test/scripts/build-in-container.sh - ARCH=x86_64 injector/test/scripts/build-in-container.sh -else - if is_remote_image "$instrumentation_image"; then - echo ---------------------------------------- - printf "using injector binary from existing remote image:\n$instrumentation_image\n" - echo ---------------------------------------- - docker pull --platform linux/arm64 "$instrumentation_image" - copy_injector_binary_from_container_image "$instrumentation_image" arm64 linux/arm64 - docker pull --platform linux/amd64 "$instrumentation_image" - copy_injector_binary_from_container_image "$instrumentation_image" x86_64 linux/amd64 - else - echo ---------------------------------------- - printf "using injector binary from existing local image:\n$instrumentation_image\n" - echo ---------------------------------------- - copy_injector_binary_from_container_image "$instrumentation_image" arm64 linux/arm64 - copy_injector_binary_from_container_image "$instrumentation_image" x86_64 linux/amd64 - fi -fi -echo - -run_tests_for_architecture_and_libc_flavor arm64 glibc -run_tests_for_architecture_and_libc_flavor x86_64 glibc -run_tests_for_architecture_and_libc_flavor arm64 musl -run_tests_for_architecture_and_libc_flavor x86_64 musl - -printf "$summary\n\n" -exit $exit_code diff --git a/images/instrumentation/injector/test/scripts/util b/images/instrumentation/injector/test/scripts/util deleted file mode 100644 index 91b2c8d0..00000000 --- a/images/instrumentation/injector/test/scripts/util +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. -# SPDX-License-Identifier: Apache-2.0 - -is_remote_image() { - image_name=${1:-} - if [[ -z "$image_name" ]]; then - echo "error: mandatory argument \"image_name\" is missing" - exit 1 - fi - - if [[ "$image_name" == *"/"* ]]; then - return 0 - else - return 1 - fi -} - -copy_injector_binary_from_container_image() { - image_name=${1:-} - if [[ -z "$image_name" ]]; then - echo "error: mandatory argument \"image_name\" is missing" - exit 1 - fi - arch=${2:-} - if [[ -z "$arch" ]]; then - echo "error: mandatory argument \"arch\" is missing" - exit 1 - fi - docker_platform=${3:-} - if [[ -z "$docker_platform" ]]; then - echo "error: mandatory argument \"docker_platform\" is missing" - exit 1 - fi - - container_id=$(docker create --platform "$docker_platform" "$image_name") - docker container cp \ - "$container_id":/dash0-init-container/dash0_injector.so \ - injector/test/bin/dash0_injector_"$arch".so - docker rm -v "$container_id" -} diff --git a/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set-but-source-attributes-unset/index.js b/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set-but-source-attributes-unset/index.js index 2abfda6a..d6cca549 100644 --- a/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set-but-source-attributes-unset/index.js +++ b/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set-but-source-attributes-unset/index.js @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 const process = require('node:process'); +const envVarName = 'OTEL_RESOURCE_ATTRIBUTES'; +const expectedValue = 'key1=value1,key2=value2'; -if (process.env["OTEL_RESOURCE_ATTRIBUTES"] !== "key1=value1,key2=value2") { - console.error(`Unexpected value for OTEL_RESOURCE_ATTRIBUTES: ${process.env["OTEL_RESOURCE_ATTRIBUTES"]}`); +if (process.env[envVarName] !== expectedValue) { + console.error(`Unexpected value for ${envVarName}: expected: '${expectedValue}'; actual: '${process.env[envVarName]}'`); process.exit(1); -} +} \ No newline at end of file diff --git a/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set/index.js b/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set/index.js index cf55c865..285ea1df 100644 --- a/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set/index.js +++ b/images/instrumentation/test/node/test-cases/otel-resource-attributes-already-set/index.js @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 const process = require('node:process'); +const envVarName = 'OTEL_RESOURCE_ATTRIBUTES'; +const expectedValue = 'k8s.namespace.name=namespace,k8s.pod.name=pod_name,k8s.pod.uid=pod_uid,k8s.container.name=container_name,key1=value1,key2=value2'; -if (process.env["OTEL_RESOURCE_ATTRIBUTES"] !== "k8s.namespace.name=namespace,k8s.pod.name=pod_name,k8s.pod.uid=pod_uid,k8s.container.name=container_name,key1=value1,key2=value2") { - console.error(`Unexpected value for OTEL_RESOURCE_ATTRIBUTES: ${process.env["OTEL_RESOURCE_ATTRIBUTES"]}`); +if (process.env[envVarName] !== expectedValue) { + console.error(`Unexpected value for ${envVarName}: expected: '${expectedValue}'; actual: '${process.env[envVarName]}'`); process.exit(1); -} +} \ No newline at end of file diff --git a/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset-and-source-attributes-unset/index.js b/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset-and-source-attributes-unset/index.js index b2f1df4c..0f600557 100644 --- a/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset-and-source-attributes-unset/index.js +++ b/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset-and-source-attributes-unset/index.js @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 const process = require('node:process'); +const envVarName = 'OTEL_RESOURCE_ATTRIBUTES'; +const expectedValue = undefined; -if (process.env["OTEL_RESOURCE_ATTRIBUTES"] !== "") { - console.error(`Unexpected value for OTEL_RESOURCE_ATTRIBUTES: ${process.env["OTEL_RESOURCE_ATTRIBUTES"]}`); +if (process.env[envVarName] !== expectedValue) { + console.error(`Unexpected value for ${envVarName}: expected: '${expectedValue}'; actual: '${process.env[envVarName]}'`); process.exit(1); } diff --git a/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset/index.js b/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset/index.js index 3214cb22..2ccf4740 100644 --- a/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset/index.js +++ b/images/instrumentation/test/node/test-cases/otel-resource-attributes-unset/index.js @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 const process = require('node:process'); +const envVarName = 'OTEL_RESOURCE_ATTRIBUTES'; +const expectedValue = "k8s.namespace.name=namespace,k8s.pod.name=pod_name,k8s.pod.uid=pod_uid,k8s.container.name=container_name"; -if (process.env["OTEL_RESOURCE_ATTRIBUTES"] !== "k8s.namespace.name=namespace,k8s.pod.name=pod_name,k8s.pod.uid=pod_uid,k8s.container.name=container_name") { - console.error(`Unexpected value for OTEL_RESOURCE_ATTRIBUTES: ${process.env["OTEL_RESOURCE_ATTRIBUTES"]}`); +if (process.env[envVarName] !== expectedValue) { + console.error(`Unexpected value for ${envVarName}: expected: '${expectedValue}'; actual: '${process.env[envVarName]}'`); process.exit(1); } diff --git a/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js b/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js index 62e56076..a443273e 100644 --- a/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js +++ b/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. // SPDX-License-Identifier: Apache-2.0 -const process = require('node:process') +const process = require('node:process'); +const envVarName = 'UNDEFINED_ENVIRONMENT_VARIABLE'; +const expectedValue = undefined; -if (process.env["UNDEFINED_ENVIRONMENT_VARIABLE"] !== undefined) { - console.error(`Unexpected value for UNDEFINED_ENVIRONMENT_VARIABLE: ${process.env["UNDEFINED_ENVIRONMENT_VARIABLE"]}`); +if (process.env[envVarName] !== expectedValue) { + console.error(`Unexpected value for ${envVarName}: expected: '${expectedValue}'; actual: '${process.env[envVarName]}'`); process.exit(1); -} +} \ No newline at end of file diff --git a/images/instrumentation/test/test-all.sh b/images/instrumentation/test/test-all.sh index 73ec357f..e1b386f7 100755 --- a/images/instrumentation/test/test-all.sh +++ b/images/instrumentation/test/test-all.sh @@ -26,7 +26,6 @@ source injector/test/scripts/util echo ---------------------------------------- instrumentation_image="dash0-instrumentation:latest" -all_docker_platforms=linux/arm64,linux/amd64 script_dir="test" exit_code=0 summary="" @@ -71,12 +70,12 @@ build_or_pull_instrumentation_image() { store_build_step_duration "pull instrumentation image" "$start_time_step" else echo ---------------------------------------- - echo "building multi-arch instrumentation image for platforms ${all_docker_platforms} from local sources" + echo "building instrumentation image for platforms ${architectures[*]} from local sources" echo ---------------------------------------- if ! docker_build_output=$( docker build \ - --platform "$all_docker_platforms" \ + --platform "${architectures[@]}" \ . \ -t "${instrumentation_image}" \ 2>&1 @@ -191,7 +190,7 @@ run_tests_for_architecture() { fi if [ "$arch" = arm64 ]; then docker_platform=linux/arm64 - elif [ "$arch" = x86_64 ]; then + elif [ "$arch" = amd64 ]; then docker_platform=linux/amd64 else echo "The architecture $arch is not supported." @@ -272,7 +271,7 @@ build_or_pull_instrumentation_image declare -a all_architectures=( "arm64" - "x86_64" + "amd64" ) for arch in "${all_architectures[@]}"; do @@ -295,4 +294,3 @@ else printf "\n${GREEN}All test cases have passed.${NC}\n" fi exit $exit_code -