From 2b897d033a56ebbf1b0597a804211a2e4890439a Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Jun 2024 15:16:07 +0200 Subject: [PATCH 01/77] Keyless Azure Auth in test-linux (#2384) * Use OIDC to login to azure * fix * Update .github/workflows/test-linux.yml * Increase timeout and re-enable azure-tests --- .github/workflows/test-linux.yml | 26 +++++++++++-------- .../Terraform/TerraformResources.cs | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index b4d75ea95..4cde21905 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -77,15 +77,18 @@ jobs: azure-tests: runs-on: ubuntu-latest needs: [ 'format', 'tests' ] - if: ${{ false }} - #if: | - # github.event_name != 'pull_request' - # || github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + if: | + github.event_name != 'pull_request' + || github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false env: ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} - ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + ARM_USE_OIDC: true + AZURE_RESOURCE_GROUP_PREFIX: ci-dotnet-${{ github.run_id }} + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -93,15 +96,16 @@ jobs: with: azure: 'true' - - name: 'Login to Azure' - run: | - az login --service-principal --username ${{ env.ARM_CLIENT_ID }} --password ${{ env.ARM_CLIENT_SECRET }} --tenant ${{ env.ARM_TENANT_ID }} - az account set --subscription ${{ env.ARM_SUBSCRIPTION_ID }} - echo "AZURE_RESOURCE_GROUP_PREFIX=ci-dotnet-${GITHUB_RUN_ID}" >> ${GITHUB_ENV} + - name: 'Az CLI login' + uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1 + with: + client-id: ${{ secrets.ARM_CLIENT_ID }} + tenant-id: ${{ secrets.ARM_TENANT_ID }} + subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }} - name: 'Tests: Azure' run: ./build.sh test --test-suite azure - + - name: 'Teardown tests infra' if: ${{ always() }} run: | diff --git a/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs index 3e21b4d91..91c671141 100644 --- a/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs +++ b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs @@ -21,7 +21,7 @@ namespace Elastic.Apm.Tests.Utilities.Terraform /// public class TerraformResources { - private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(15); + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(180); private readonly string _resourceDirectory; private readonly IMessageSink _messageSink; From d99347d7f7dc169abde5069c090513bd8da33fe5 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Jul 2024 13:10:57 +0100 Subject: [PATCH 02/77] Fix linux build dependency of glibc (#2389) --- .../workflows/install-dependencies/action.yml | 161 ++++++++++-------- CONTRIBUTING.md | 12 +- Makefile.toml | 11 +- build/scripts/Build.fs | 37 ++-- docs/setup-auto-instrumentation.asciidoc | 8 +- src/profiler/elastic_apm_profiler/Cargo.toml | 2 +- .../ProfiledApplication.cs | 4 +- 7 files changed, 133 insertions(+), 102 deletions(-) diff --git a/.github/workflows/install-dependencies/action.yml b/.github/workflows/install-dependencies/action.yml index 10552af91..df05f3bde 100644 --- a/.github/workflows/install-dependencies/action.yml +++ b/.github/workflows/install-dependencies/action.yml @@ -1,78 +1,89 @@ --- -name: Install Dependencies -description: Ensures an action has the appropiate non .NET dependencies installed - -inputs: - rust: - description: 'Install rust toolchain ("true" or "false")' - required: false - default: "false" - azure: - description: 'Install azure functions tool chain ("true" or "false")' - required: false - default: "false" - tc-cloud: - description: 'Bootstrap TestContainers Cloud (TOKEN or "false")' - required: false - default: "false" - -runs: - using: "composite" - steps: - # RUST - - name: Rustup - if: "${{ inputs.rust == 'true' }}" - shell: bash - run: rustup default 1.69.0 - - # - name: Cargo make - # if: "${{ inputs.rust == 'true' }}" - # shell: bash - # run: cargo install --force cargo-make - - - name: Install cargo-make using cache - if: "${{ inputs.rust == 'true' }}" - uses: baptiste0928/cargo-install@v3 - with: - crate: cargo-make - version: "^0.36.8" - - - uses: Swatinem/rust-cache@v2 - if: "${{ inputs.rust == 'true' }}" - with: - cache-targets: "false" - cache-all-crates: "true" - + name: Install Dependencies + description: Ensures an action has the appropiate non .NET dependencies installed + + inputs: + rust: + description: 'Install rust toolchain ("true" or "false")' + required: false + default: "false" + azure: + description: 'Install azure functions tool chain ("true" or "false")' + required: false + default: "false" + tc-cloud: + description: 'Bootstrap TestContainers Cloud (TOKEN or "false")' + required: false + default: "false" - # AZURE - - name: 'Linux: Azure functions core tools' - if: "${{ inputs.azure == 'true' && runner.os == 'Linux' }}" - shell: bash - run: | - wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb - sudo dpkg -i packages-microsoft-prod.deb - sudo apt-get update - sudo apt-get install azure-functions-core-tools-4 + runs: + using: "composite" + steps: + # ZIG + - name: Zig + if: "${{ inputs.rust == 'true' && runner.os == 'Linux' }}" + shell: bash + run: sudo snap install zig --beta --classic + + # RUST + - name: Rustup + if: "${{ inputs.rust == 'true' }}" + shell: bash + run: rustup default 1.79.0 + + # - name: Cargo make + # if: "${{ inputs.rust == 'true' }}" + # shell: bash + # run: cargo install --force cargo-make + + - name: Install cargo-make using cache + if: "${{ inputs.rust == 'true' }}" + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-make + version: "^0.36.8" + + - name: Install cargo zigbuild + if: "${{ inputs.rust == 'true' && runner.os == 'Linux' }}" + shell: bash + run: cargo install --force cargo-zigbuild + + - uses: Swatinem/rust-cache@v2 + if: "${{ inputs.rust == 'true' }}" + with: + cache-targets: "false" + cache-all-crates: "true" + + # AZURE + - name: 'Linux: Azure functions core tools' + if: "${{ inputs.azure == 'true' && runner.os == 'Linux' }}" + shell: bash + run: | + wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install azure-functions-core-tools-4 + + - name: 'Windows: Azure functions core tools' + if: "${{ inputs.azure == 'true' && runner.os == 'Windows' }}" + shell: cmd + run: choco install azure-functions-core-tools -y --no-progress -r --version 4.0.4829 - - name: 'Windows: Azure functions core tools' - if: "${{ inputs.azure == 'true' && runner.os == 'Windows' }}" - shell: cmd - run: choco install azure-functions-core-tools -y --no-progress -r --version 4.0.4829 - - # TEST CONTAINERS CLOUD - # If no PR event or if a PR event that's caused by a non-fork and non dependabot actor - - name: Setup TestContainers Cloud Client - if: | - inputs.tc-cloud != 'false' - && (github.event_name != 'pull_request' - || (github.event_name == 'pull_request' - && github.event.pull_request.head.repo.fork == false - && github.actor != 'dependabot[bot]' - ) - ) - uses: atomicjar/testcontainers-cloud-setup-action@c335bdbb570ec7c48f72c7d450c077f0a002293e # v1.3.0 - with: - token: ${{ inputs.tc-cloud }} - - - + # TEST CONTAINERS CLOUD + # If no PR event or if a PR event that's caused by a non-fork and non dependabot actor + - name: Setup TestContainers Cloud Client + if: | + inputs.tc-cloud != 'false' + && (github.event_name != 'pull_request' + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.fork == false + && github.actor != 'dependabot[bot]' + ) + ) + uses: atomicjar/testcontainers-cloud-setup-action@c335bdbb570ec7c48f72c7d450c077f0a002293e # v1.3.0 + with: + token: ${{ inputs.tc-cloud }} + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3a24839d..ce9e9c06c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,18 +15,22 @@ feedback and ideas are always welcome. ### .NET source In order to build the .NET source code, you'll need - * [.NET 6.0 or later](https://dotnet.microsoft.com/download/dotnet/6.0) + * [.NET 8.0 or later](https://dotnet.microsoft.com/download/dotnet/8.0) * **If** you're running on Windows **and** also wish to build projects that target .NET Framework, -you'll need a minimum of .NET Framework 4.6.1 installed. +you'll need a minimum of .NET Framework 4.6.2 installed. You can use any IDE that supports .NET development, and you can use any OS that is supported by .NET. ### Rust source -In order to build the CLR profiler source code, you'll need - * [Rust 1.54 or later](https://www.rust-lang.org/tools/install) +In order to build the CLR profiler source code, you'll need: + * [Rust 1.79 or later](https://www.rust-lang.org/tools/install) * [Cargo make](https://github.com/sagiegurari/cargo-make#installation) + On Linux, you will also require: + * [Cargo zigbuild](https://github.com/rust-cross/cargo-zigbuild) + * [Zig](https://github.com/ziglang/zig) + You can use any IDE that supports Rust development; we typically use [CLion](https://www.jetbrains.com/clion/) with the [Rust plugin](https://plugins.jetbrains.com/plugin/8182-rust/docs), or [VS Code](https://code.visualstudio.com/) diff --git a/Makefile.toml b/Makefile.toml index 20bcfe1ae..539cac4c1 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -35,6 +35,15 @@ description = "Builds CLR Profiler for release" # loader assembly is embedded in the profiler dependencies = ["build-loader"] +[tasks.build-release-linux] +description = "Builds CLR Profiler for release against a known version of glibc" +install_crate = false +toolchain = "${CARGO_MAKE_RUST_DEFAULT_TOOLCHAIN}" +command = "cargo" +args = ["zigbuild", "--release", "--target", "x86_64-unknown-linux-gnu.2.14", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] +# loader assembly is embedded in the profiler +dependencies = ["build-loader"] + [tasks.build-integrations] description = "Builds Managed Profiler .NET integrations" command = "dotnet" @@ -83,7 +92,7 @@ cargo expand --manifest-path src/profiler/elastic_apm_profiler/Cargo.toml --colo [tasks.set-profiler-env] private = true -env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}/target/release/libelastic_apm_profiler.so" } +env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}/target/x86_64-unknown-linux-gnu/release/libelastic_apm_profiler.so" } [tasks.set-profiler-env.mac] env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}/target/release/libelastic_apm_profiler.dylib" } diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index 58af804bf..e6f9954fb 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -186,20 +186,20 @@ module Build = @ ["--"; "RunConfiguration.CollectSourceInformation=true"] DotNet.ExecWithTimeout command (TimeSpan.FromMinutes 30) - - + /// Builds the CLR profiler and supporting .NET managed assemblies let BuildProfiler () = dotnet "build" (Paths.ProfilerProjFile "Elastic.Apm.Profiler.Managed") - Cargo.Exec [ "make"; "build-release"; "--disable-check-for-update"] - + if isWindows then Cargo.Exec [ "make"; "build-release"; "--disable-check-for-update"] + else Cargo.Exec [ "make"; "build-release-linux"; "--disable-check-for-update"] + /// Publishes all projects with framework versions - let Publish targets = + let Publish targets = let projs = match targets with | Some t -> t | None -> allSrcProjects - + projs |> Seq.map getAllTargetFrameworks |> Seq.iter (fun (proj, frameworks) -> @@ -290,7 +290,6 @@ module Build = // include version in the zip file name ZipFile.CreateFromDirectory(agentDir.FullName, Paths.BuildOutput versionedName + ".zip") - let ProfilerIntegrations () = DotNet.Exec ["run"; "--project"; Paths.ProfilerProjFile "Elastic.Apm.Profiler.IntegrationsGenerator"; "--" "-i"; Paths.SrcProfiler "Elastic.Apm.Profiler.Managed/bin/Release/netstandard2.0/Elastic.Apm.Profiler.Managed.dll" @@ -299,20 +298,25 @@ module Build = /// Creates versioned elastic_apm_profiler.zip file containing all components needed for profiler auto-instrumentation let ProfilerZip () = let name = "elastic_apm_profiler" + let directory = Paths.BuildOutput name + + if Directory.Exists(directory) then + Directory.Delete(directory, true) + let currentAssemblyVersion = Versioning.CurrentVersion.FileVersion let versionedName = let os = if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then "win-x64" - else "linux-x64" + else "linux-x64" sprintf "%s_%s-%s" name (currentAssemblyVersion.ToString()) os - let profilerDir = Paths.BuildOutput name |> DirectoryInfo + let profilerDir = directory |> DirectoryInfo profilerDir.Create() - + seq { Paths.SrcProfiler "Elastic.Apm.Profiler.Managed/integrations.yml" "target/release/elastic_apm_profiler.dll" - "target/release/libelastic_apm_profiler.so" + "target/x86_64-unknown-linux-gnu/release/libelastic_apm_profiler.so" Paths.SrcProfiler "elastic_apm_profiler/NOTICE" Paths.SrcProfiler "elastic_apm_profiler/README" "LICENSE" @@ -321,12 +325,13 @@ module Build = |> Seq.filter (fun file -> file.Exists) |> Seq.iter (fun file -> let destination = Path.combine profilerDir.FullName file.Name - let newFile = file.CopyTo(destination, true) + let newFile = file.CopyTo(destination, true) if newFile.Name = "README" then File.applyReplace (fun s -> s.Replace("${VERSION}", sprintf "%i.%i" currentAssemblyVersion.Major currentAssemblyVersion.Minor)) newFile.FullName ) - + Directory.GetDirectories((Paths.BuildOutput "Elastic.Apm.Profiler.Managed"), "*", SearchOption.TopDirectoryOnly) + |> Array.filter (fun dir -> isWindows || not (dir.EndsWith("net462"))) |> Seq.map DirectoryInfo |> Seq.iter (fun sourceDir -> copyDllsAndPdbs (profilerDir.CreateSubdirectory(sourceDir.Name)) sourceDir) @@ -335,8 +340,4 @@ module Build = if File.exists zip then printf $"%s{zip} already exists on disk" File.delete zip - ZipFile.CreateFromDirectory(profilerDir.FullName, zip) - - - - \ No newline at end of file + ZipFile.CreateFromDirectory(profilerDir.FullName, zip) \ No newline at end of file diff --git a/docs/setup-auto-instrumentation.asciidoc b/docs/setup-auto-instrumentation.asciidoc index ca5e49b77..19ae58724 100644 --- a/docs/setup-auto-instrumentation.asciidoc +++ b/docs/setup-auto-instrumentation.asciidoc @@ -16,7 +16,7 @@ This approach works with the following |=== | 2.+^|**Operating system** -|**Architecture** |**Windows** |**Linux** +|**Architecture** |**Windows** |**Linux** ** |x64 @@ -33,7 +33,11 @@ This approach works with the following _* Due to binding issues introduced by Microsoft, we recommend at least .NET Framework 4.7.2 for best compatibility._ -NOTE: The Profiler based agent only supports 64-bit applications. 32-bit applications aren't supported. +_** Minimum GLIBC version 2.14._ + +NOTE: The profiler-based agent only supports 64-bit applications. 32-bit applications aren't supported. + +NOTE: The profiler-based agent does not currently support ARM. It instruments the following assemblies: diff --git a/src/profiler/elastic_apm_profiler/Cargo.toml b/src/profiler/elastic_apm_profiler/Cargo.toml index 61c594288..b5e1e8551 100644 --- a/src/profiler/elastic_apm_profiler/Cargo.toml +++ b/src/profiler/elastic_apm_profiler/Cargo.toml @@ -14,7 +14,7 @@ c_vec = "2.0.0" com = { version = "0.6.0", features = ["production"] } hex = "0.4.3" log = "0.4.14" -log4rs = { version = "1.0.0", default_features = false, features = ["console_appender", "rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] } +log4rs = { version = "1.0.0", default-features = false, features = ["console_appender", "rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] } num-derive = "0.3" num-traits = "0.2" once_cell = "1.8.0" diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs index 274ed3858..5e6fa0c81 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs @@ -55,7 +55,9 @@ protected ProfiledApplication(string projectName, params string[] folders) else profilerFile = "libelastic_apm_profiler.dylib"; - _profilerPath = Path.Combine(SolutionPaths.Root, "target", "release", profilerFile); + _profilerPath = TestEnvironment.IsLinux ? + Path.Combine(SolutionPaths.Root, "target", "x86_64-unknown-linux-gnu", "release", profilerFile) : + Path.Combine(SolutionPaths.Root, "target", "release", profilerFile); if (!File.Exists(_profilerPath)) { From 42751c3f82a846f93b4ffa0ff2ae25443f882424 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Jul 2024 14:43:25 +0100 Subject: [PATCH 03/77] Update changelog for 1.28.0 release (#2390) --- CHANGELOG.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index ac98071c7..ad9e0e69e 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,13 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.0]] +==== 1.28.0 - 2024/07/03 + +===== Bug fixes + +{pull}2389[#2389] Fix Linux build dependency of glibc + [[release-notes-1.27.3]] ==== 1.27.3 - 2024/06/18 From 980658eff6bd1635313ddcef97ea702cfc9cccdb Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 9 Jul 2024 12:52:40 +0200 Subject: [PATCH 04/77] updatecli: use shared policy (#2393) --- .../updatecli.d/update-gherkin-specs.yml | 83 ------------------ .../updatecli.d/update-json-specs.yml | 83 ------------------ .ci/updatecli/updatecli.d/update-specs.yml | 85 ------------------- .ci/updatecli/values.d/apm-data-spec.yml | 1 + .ci/updatecli/values.d/apm-gherkin.yml | 1 + .ci/updatecli/values.d/apm-json-specs.yml | 1 + .ci/updatecli/values.d/scm.yml | 7 ++ .ci/updatecli/values.yml | 14 --- .github/workflows/updatecli.yml | 19 ++++- update-compose.yaml | 18 ++++ 10 files changed, 45 insertions(+), 267 deletions(-) delete mode 100644 .ci/updatecli/updatecli.d/update-gherkin-specs.yml delete mode 100644 .ci/updatecli/updatecli.d/update-json-specs.yml delete mode 100644 .ci/updatecli/updatecli.d/update-specs.yml create mode 100644 .ci/updatecli/values.d/apm-data-spec.yml create mode 100644 .ci/updatecli/values.d/apm-gherkin.yml create mode 100644 .ci/updatecli/values.d/apm-json-specs.yml create mode 100644 .ci/updatecli/values.d/scm.yml delete mode 100644 .ci/updatecli/values.yml create mode 100644 update-compose.yaml diff --git a/.ci/updatecli/updatecli.d/update-gherkin-specs.yml b/.ci/updatecli/updatecli.d/update-gherkin-specs.yml deleted file mode 100644 index 13f0617a9..000000000 --- a/.ci/updatecli/updatecli.d/update-gherkin-specs.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: update-gherkin-specs -pipelineid: update-gherkin-specs - -scms: - default: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - commitusingapi: true - apm: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.apm_repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - -sources: - sha: - kind: file - spec: - file: 'https://github.com/{{ .github.owner }}/{{ .github.apm_repository }}/commit/{{ .github.branch }}.patch' - matchpattern: "^From\\s([0-9a-f]{40})\\s" - transformers: - - findsubmatch: - pattern: "[0-9a-f]{40}" - pull_request: - kind: shell - dependson: - - sha - spec: - command: gh api /repos/{{ .github.owner }}/{{ .github.apm_repository }}/commits/{{ source "sha" }}/pulls --jq '.[].html_url' - environments: - - name: GITHUB_TOKEN - - name: PATH - agents-gherkin-specs-tarball: - kind: shell - scmid: apm - dependson: - - sha - spec: - command: tar cvzf {{ requiredEnv "GITHUB_WORKSPACE" }}/gherkin-specs.tgz . - environments: - - name: PATH - workdir: "{{ .specs.apm_gherkin_path }}" - -actions: - pr: - kind: "github/pullrequest" - scmid: default - spec: - automerge: false - draft: false - labels: - - "automation" - description: |- - ### What - APM agent Gherkin specs automatic sync - - ### Why - *Changeset* - * {{ source "pull_request" }} - * https://github.com/elastic/apm/commit/{{ source "sha" }} - title: '[Automation] Update Gherkin specs' - -targets: - agent-gherkin-specs: - name: APM agent gherkin specs {{ source "sha" }} - scmid: default - disablesourceinput: true - kind: shell - spec: - # git diff helps to print what it changed, If it is empty, then updatecli report a success with no changes applied. - # See https://www.updatecli.io/docs/plugins/resource/shell/#_shell_target - command: 'tar -xzf {{ requiredEnv "GITHUB_WORKSPACE" }}/gherkin-specs.tgz && git --no-pager diff' - workdir: "{{ .apm_agent.gherkin_specs_path }}" diff --git a/.ci/updatecli/updatecli.d/update-json-specs.yml b/.ci/updatecli/updatecli.d/update-json-specs.yml deleted file mode 100644 index ae9816775..000000000 --- a/.ci/updatecli/updatecli.d/update-json-specs.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: update-json-specs -pipelineid: update-json-specs - -scms: - default: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - commitusingapi: true - apm: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.apm_repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - -sources: - sha: - kind: file - spec: - file: 'https://github.com/{{ .github.owner }}/{{ .github.apm_repository }}/commit/{{ .github.branch }}.patch' - matchpattern: "^From\\s([0-9a-f]{40})\\s" - transformers: - - findsubmatch: - pattern: "[0-9a-f]{40}" - pull_request: - kind: shell - dependson: - - sha - spec: - command: gh api /repos/{{ .github.owner }}/{{ .github.apm_repository }}/commits/{{ source "sha" }}/pulls --jq '.[].html_url' - environments: - - name: GITHUB_TOKEN - - name: PATH - agents-json-specs-tarball: - kind: shell - scmid: apm - dependson: - - sha - spec: - command: tar cvzf {{ requiredEnv "GITHUB_WORKSPACE" }}/json-specs.tgz . - environments: - - name: PATH - workdir: "{{ .specs.apm_json_path }}" - -actions: - pr: - kind: "github/pullrequest" - scmid: default - spec: - automerge: false - draft: false - labels: - - "automation" - description: |- - ### What - APM agent specs automatic sync - - ### Why - *Changeset* - * {{ source "pull_request" }} - * https://github.com/{{ .github.owner }}/{{ .github.apm_repository }}/commit/{{ source "sha" }} - title: '[Automation] Update JSON specs' - -targets: - agent-json-specs: - name: APM agent json specs {{ source "sha" }} - scmid: default - disablesourceinput: true - kind: shell - spec: - # git diff helps to print what it changed, If it is empty, then updatecli report a success with no changes applied. - # See https://www.updatecli.io/docs/plugins/resource/shell/#_shell_target - command: 'tar -xzf {{ requiredEnv "GITHUB_WORKSPACE" }}/json-specs.tgz && git --no-pager diff' - workdir: "{{ .apm_agent.json_specs_path }}" diff --git a/.ci/updatecli/updatecli.d/update-specs.yml b/.ci/updatecli/updatecli.d/update-specs.yml deleted file mode 100644 index e9bbaf945..000000000 --- a/.ci/updatecli/updatecli.d/update-specs.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: update-specs -pipelineid: update-schema-specs - -scms: - default: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - commitusingapi: true - - apm-data: - kind: github - spec: - user: '{{ requiredEnv "GITHUB_ACTOR" }}' - owner: "{{ .github.owner }}" - repository: "{{ .github.apm_data_repository }}" - token: '{{ requiredEnv "GITHUB_TOKEN" }}' - username: '{{ requiredEnv "GITHUB_ACTOR" }}' - branch: "{{ .github.branch }}" - -sources: - sha: - kind: file - spec: - file: 'https://github.com/{{ .github.owner }}/{{ .github.apm_data_repository }}/commit/{{ .github.branch }}.patch' - matchpattern: "^From\\s([0-9a-f]{40})\\s" - transformers: - - findsubmatch: - pattern: "[0-9a-f]{40}" - pull_request: - kind: shell - dependson: - - sha - spec: - command: gh api /repos/{{ .github.owner }}/{{ .github.apm_data_repository }}/commits/{{ source "sha" }}/pulls --jq '.[].html_url' - environments: - - name: GITHUB_TOKEN - - name: PATH - agent-specs-tarball: - kind: shell - scmid: apm-data - dependson: - - sha - spec: - command: tar cvzf {{ requiredEnv "GITHUB_WORKSPACE" }}/json-schema.tgz . - environments: - - name: PATH - workdir: "{{ .specs.apm_data_path }}" - -actions: - pr: - kind: "github/pullrequest" - scmid: default - sourceid: sha - spec: - automerge: false - draft: false - labels: - - "automation" - description: |- - ### What - APM agent json server schema automatic sync - - ### Why - *Changeset* - * {{ source "pull_request" }} - * https://github.com/{{ .github.owner }}/{{ .github.apm_data_repository }}/commit/{{ source "sha" }} - title: '[Automation] Update JSON server schema specs' - -targets: - agent-json-schema: - name: APM agent json server schema {{ source "sha" }} - scmid: default - disablesourceinput: true - kind: shell - spec: - # git diff helps to print what it changed, If it is empty, then updatecli report a success with no changes applied. - # See https://www.updatecli.io/docs/plugins/resource/shell/#_shell_target - command: 'tar -xzf {{ requiredEnv "GITHUB_WORKSPACE" }}/json-schema.tgz && git --no-pager diff' - workdir: "{{ .apm_agent.server_schema_specs_path }}" diff --git a/.ci/updatecli/values.d/apm-data-spec.yml b/.ci/updatecli/values.d/apm-data-spec.yml new file mode 100644 index 000000000..eba382561 --- /dev/null +++ b/.ci/updatecli/values.d/apm-data-spec.yml @@ -0,0 +1 @@ +apm_schema_specs_path: src/Elastic.Apm.Specification/specs diff --git a/.ci/updatecli/values.d/apm-gherkin.yml b/.ci/updatecli/values.d/apm-gherkin.yml new file mode 100644 index 000000000..86dace3dc --- /dev/null +++ b/.ci/updatecli/values.d/apm-gherkin.yml @@ -0,0 +1 @@ +apm_gherkin_specs_path: test/Elastic.Apm.Feature.Tests/Features diff --git a/.ci/updatecli/values.d/apm-json-specs.yml b/.ci/updatecli/values.d/apm-json-specs.yml new file mode 100644 index 000000000..27e728e1c --- /dev/null +++ b/.ci/updatecli/values.d/apm-json-specs.yml @@ -0,0 +1 @@ +apm_json_specs_path: test/Elastic.Apm.Tests.Utilities/TestResources/json-specs diff --git a/.ci/updatecli/values.d/scm.yml b/.ci/updatecli/values.d/scm.yml new file mode 100644 index 000000000..876325fb5 --- /dev/null +++ b/.ci/updatecli/values.d/scm.yml @@ -0,0 +1,7 @@ +scm: + enabled: true + owner: elastic + repository: apm-agent-dotnet + branch: main + +signedcommit: true \ No newline at end of file diff --git a/.ci/updatecli/values.yml b/.ci/updatecli/values.yml deleted file mode 100644 index 575ba4a45..000000000 --- a/.ci/updatecli/values.yml +++ /dev/null @@ -1,14 +0,0 @@ -github: - owner: "elastic" - repository: "apm-agent-dotnet" - apm_repository: "apm" - apm_data_repository: "apm-data" - branch: "main" -specs: - apm_data_path: "input/elasticapm/docs/spec/v2" - apm_json_path: "tests/agents/json-specs" - apm_gherkin_path: "tests/agents/gherkin-specs" -apm_agent: - gherkin_specs_path: "test/Elastic.Apm.Feature.Tests/Features" - json_specs_path: "test/Elastic.Apm.Tests.Utilities/TestResources/json-specs" - server_schema_specs_path: "src/Elastic.Apm.Specification/specs" \ No newline at end of file diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 3c667994c..7103377de 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -9,14 +9,29 @@ permissions: contents: read jobs: - bump: + compose: runs-on: ubuntu-latest + permissions: + contents: read + packages: read steps: - uses: actions/checkout@v4 + - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: elastic/oblt-actions/updatecli/run@v1 + with: + command: --experimental compose diff + env: + GITHUB_TOKEN: ${{ secrets.UPDATECLI_GH_TOKEN }} + - uses: elastic/oblt-actions/updatecli/run@v1 with: - command: "--experimental apply --config .ci/updatecli/updatecli.d --values .ci/updatecli/values.yml" + command: --experimental compose apply env: GITHUB_TOKEN: ${{ secrets.UPDATECLI_GH_TOKEN }} diff --git a/update-compose.yaml b/update-compose.yaml new file mode 100644 index 000000000..d40020933 --- /dev/null +++ b/update-compose.yaml @@ -0,0 +1,18 @@ +policies: + - name: Handle apm-data server specs + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-data-spec:0.2.0@sha256:7069c0773d44a74c4c8103b4d9957b468f66081ee9d677238072fe11c4d2197c + values: + - .ci/updatecli/values.d/scm.yml + - .ci/updatecli/values.d/apm-data-spec.yml + + - name: Handle apm gherkin specs + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-gherkin:0.2.0@sha256:26a30ad2b98a6e4cb17fb88a28fa3277ced8ca862d6388943afaafbf8ee96e7d + values: + - .ci/updatecli/values.d/scm.yml + - .ci/updatecli/values.d/apm-gherkin.yml + + - name: Handle apm json specs + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-json-specs:0.2.0@sha256:969a6d21eabd6ebea66cb29b35294a273d6dbc0f7da78589c416aedf08728e78 + values: + - .ci/updatecli/values.d/scm.yml + - .ci/updatecli/values.d/apm-json-specs.yml From a5401687ca1ff506f39bef5cf01b8008c0d16bf2 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Wed, 10 Jul 2024 17:27:51 +0200 Subject: [PATCH 05/77] feat: make published Docker images multi-platform, add linux/arm64 plat (#2395) --- .github/workflows/release-main.yml | 67 ++++++++++++++++++++++++++++-- .github/workflows/release.yml | 1 + 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 04c65a048..bd5b886b5 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -17,7 +17,12 @@ env: jobs: release: runs-on: ubuntu-latest - + env: + DOCKER_IMAGE_NAME: "docker.elastic.co/observability/apm-agent-dotnet" + PREFIX_APM_AGENT: "build/output/ElasticApmAgent_" + PREFIX_APM_PROFILER: "build/output/elastic_apm_profiler_" + SUFFIX_APM_AGENT: ".zip" + SUFFIX_APM_PROFILER: "-linux-x64.zip" steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -40,9 +45,65 @@ jobs: - name: publish canary packages github package repository run: dotnet nuget push 'build/output/_packages/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} -s https://nuget.pkg.github.com/elastic/index.json --skip-duplicate --no-symbols - + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to the Elastic Container registry + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: ${{ secrets.ELASTIC_DOCKER_REGISTRY }} + username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} + password: ${{ secrets.ELASTIC_DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) + id: docker-meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.DOCKER_IMAGE_NAME }} + flavor: | + latest=auto + tags: | + # "1.2.3" and "latest" Docker tags on push of git tag "v1.2.3" + type=raw,value=${{ steps.bootstrap.outputs.agent-version }} + # "edge" Docker tag on git push to default branch + type=edge + + - name: Build and Push Profiler Docker Image + id: docker-push + continue-on-error: true # continue for now until we see it working in action + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0 + with: + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} + + - name: Attest image + uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + continue-on-error: true # continue for now until we see it working in action + with: + subject-name: ${{ env.DOCKER_IMAGE_NAME }} + subject-digest: ${{ steps.docker-push.outputs.digest }} + push-to-registry: true + + - name: generate build provenance (APM Agent) + uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + with: + subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" + + - name: generate build provenance (APM Profiler) + uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + with: + subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" + - if: ${{ failure() }} - uses: elastic/oblt-actions/slack/send@v1.7.0 + uses: elastic/oblt-actions/slack/send@v1 with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} channel-id: "#apm-agent-dotnet" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c98d6baa..af776a759 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,6 +72,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.docker-meta.outputs.tags }} labels: ${{ steps.docker-meta.outputs.labels }} From 78b80113b26b8599cbffa410f89a954a2cd91f9f Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 16 Jul 2024 20:52:06 +0200 Subject: [PATCH 06/77] release: fix adding latest tag (#2399) * release: fix adding latest tag * Fix docker metadata action configuration * remove extra whitespace --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af776a759..a8ad00f38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: latest=auto tags: | # "1.2.3" and "latest" Docker tags on push of git tag "v1.2.3" - type=raw,value=${{ steps.bootstrap.outputs.agent-version }} + type=semver,pattern={{version}},value=${{ steps.bootstrap.outputs.agent-version }} # "edge" Docker tag on git push to default branch type=edge From 7b2dc2bd12f0cba3232c2b10978ebbe46549f557 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 18 Jul 2024 13:06:32 +0200 Subject: [PATCH 07/77] Introduce IHostNameDetector to speed up tests (#2398) * Introduce IHostNameDetector to speed up tests * move new components to their own files --- src/Elastic.Apm/AgentComponents.cs | 8 +- src/Elastic.Apm/Helpers/HostNameDetector.cs | 91 +++++++++++++++++++ src/Elastic.Apm/Helpers/SystemInfoHelper.cs | 78 +--------------- .../TestAgentComponents.cs | 5 +- .../TestHostNameDetector.cs | 23 +++++ .../CentralConfigFetcherTests.cs | 4 +- test/Elastic.Apm.Tests/SerializationTests.cs | 2 +- .../SystemInfoHelperTests.cs | 4 +- .../TestsBase.cs | 2 +- .../ContainerIdCalculationTests.cs | 4 +- 10 files changed, 133 insertions(+), 88 deletions(-) create mode 100644 src/Elastic.Apm/Helpers/HostNameDetector.cs create mode 100644 test/Elastic.Apm.Tests.Utilities/TestHostNameDetector.cs diff --git a/src/Elastic.Apm/AgentComponents.cs b/src/Elastic.Apm/AgentComponents.cs index 24e57e9bf..917a747d7 100644 --- a/src/Elastic.Apm/AgentComponents.cs +++ b/src/Elastic.Apm/AgentComponents.cs @@ -43,12 +43,14 @@ internal AgentComponents( ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer, ICentralConfigurationFetcher centralConfigurationFetcher, IApmServerInfo apmServerInfo, - BreakdownMetricsProvider breakdownMetricsProvider = null + BreakdownMetricsProvider breakdownMetricsProvider = null, + IHostNameDetector hostNameDetector = null ) { try { var config = CreateConfiguration(logger, configurationReader); + hostNameDetector ??= new HostNameDetector(); Logger = logger ?? CheckForProfilerLogger(DefaultLogger(null, configurationReader), config.LogLevel); ConfigurationStore = new ConfigurationStore(new RuntimeConfigurationSnapshot(config), Logger); @@ -64,7 +66,7 @@ internal AgentComponents( ElasticActivityListener = new ElasticActivityListener(this, HttpTraceConfiguration); #endif var systemInfoHelper = new SystemInfoHelper(Logger); - var system = systemInfoHelper.GetSystemInfo(Configuration.HostName); + var system = systemInfoHelper.GetSystemInfo(Configuration.HostName, hostNameDetector); PayloadSender = payloadSender ?? new PayloadSenderV2(Logger, ConfigurationStore.CurrentSnapshot, Service, system, @@ -147,7 +149,7 @@ private void ServerInfoCallback(bool success, IApmServerInfo serverInfo) return; #else if (!Configuration.OpenTelemetryBridgeEnabled) return; - + if (success) { if (serverInfo.Version >= new ElasticVersion(7, 16, 0, string.Empty)) diff --git a/src/Elastic.Apm/Helpers/HostNameDetector.cs b/src/Elastic.Apm/Helpers/HostNameDetector.cs new file mode 100644 index 000000000..a7aeb9ed6 --- /dev/null +++ b/src/Elastic.Apm/Helpers/HostNameDetector.cs @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Net; +using System.Net.NetworkInformation; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Helpers; + +internal interface IHostNameDetector +{ + string GetDetectedHostName(IApmLogger logger); +} +public class HostNameDetector : IHostNameDetector +{ + public string GetDetectedHostName(IApmLogger logger) + { + var fqdn = string.Empty; + + try + { + fqdn = Dns.GetHostEntry(string.Empty).HostName; + } + catch (Exception e) + { + logger.Warning()?.LogException(e, "Failed to get hostname via Dns.GetHostEntry(string.Empty).HostName."); + } + + if (!string.IsNullOrEmpty(fqdn)) + return NormalizeHostName(fqdn); + + try + { + var hostName = IPGlobalProperties.GetIPGlobalProperties().HostName; + var domainName = IPGlobalProperties.GetIPGlobalProperties().DomainName; + + if (!string.IsNullOrEmpty(domainName)) + { + hostName = $"{hostName}.{domainName}"; + } + + fqdn = hostName; + + } + catch (Exception e) + { + logger.Warning()?.LogException(e, "Failed to get hostname via IPGlobalProperties.GetIPGlobalProperties()."); + } + + if (!string.IsNullOrEmpty(fqdn)) + return NormalizeHostName(fqdn); + + try + { + fqdn = Environment.MachineName; + } + catch (Exception e) + { + logger.Warning()?.LogException(e, "Failed to get hostname via Environment.MachineName."); + } + + if (!string.IsNullOrEmpty(fqdn)) + return NormalizeHostName(fqdn); + + logger.Debug()?.Log("Falling back to environment variables to get hostname."); + + try + { + fqdn = (Environment.GetEnvironmentVariable("COMPUTERNAME") + ?? Environment.GetEnvironmentVariable("HOSTNAME")) + ?? Environment.GetEnvironmentVariable("HOST"); + + if (string.IsNullOrEmpty(fqdn)) + logger.Error()?.Log("Failed to get hostname via environment variables."); + + return NormalizeHostName(fqdn); + } + catch (Exception e) + { + logger.Error()?.LogException(e, "Failed to get hostname."); + } + + return null; + + static string NormalizeHostName(string hostName) => + string.IsNullOrEmpty(hostName) ? null : hostName.Trim().ToLower(); + } +} diff --git a/src/Elastic.Apm/Helpers/SystemInfoHelper.cs b/src/Elastic.Apm/Helpers/SystemInfoHelper.cs index 6c1371aa1..c737dbb11 100644 --- a/src/Elastic.Apm/Helpers/SystemInfoHelper.cs +++ b/src/Elastic.Apm/Helpers/SystemInfoHelper.cs @@ -6,8 +6,6 @@ using System.Data.Common; using System.IO; using System.Linq; -using System.Net; -using System.Net.NetworkInformation; using System.Text.RegularExpressions; using Elastic.Apm.Api; using Elastic.Apm.Api.Kubernetes; @@ -109,9 +107,9 @@ internal void ParseContainerId(Api.System system, string reportedHostName, strin _logger.Info()?.Log("Could not parse container ID from '/proc/self/cgroup' line: {line}", line); } - internal Api.System GetSystemInfo(string hostName) + internal Api.System GetSystemInfo(string hostName, IHostNameDetector detector) { - var detectedHostName = GetHostName(); + var detectedHostName = detector.GetDetectedHostName(_logger); var system = new Api.System { DetectedHostName = detectedHostName, ConfiguredHostName = hostName }; if (AgentFeaturesProvider.Get(_logger).Check(AgentFeature.ContainerInfo)) @@ -123,78 +121,6 @@ internal Api.System GetSystemInfo(string hostName) return system; } - internal string GetHostName() - { - var fqdn = string.Empty; - - try - { - fqdn = Dns.GetHostEntry(string.Empty).HostName; - } - catch (Exception e) - { - _logger.Warning()?.LogException(e, "Failed to get hostname via Dns.GetHostEntry(string.Empty).HostName."); - } - - if (!string.IsNullOrEmpty(fqdn)) - return NormalizeHostName(fqdn); - - try - { - var hostName = IPGlobalProperties.GetIPGlobalProperties().HostName; - var domainName = IPGlobalProperties.GetIPGlobalProperties().DomainName; - - if (!string.IsNullOrEmpty(domainName)) - { - hostName = $"{hostName}.{domainName}"; - } - - fqdn = hostName; - - } - catch (Exception e) - { - _logger.Warning()?.LogException(e, "Failed to get hostname via IPGlobalProperties.GetIPGlobalProperties()."); - } - - if (!string.IsNullOrEmpty(fqdn)) - return NormalizeHostName(fqdn); - - try - { - fqdn = Environment.MachineName; - } - catch (Exception e) - { - _logger.Warning()?.LogException(e, "Failed to get hostname via Environment.MachineName."); - } - - if (!string.IsNullOrEmpty(fqdn)) - return NormalizeHostName(fqdn); - - _logger.Debug()?.Log("Falling back to environment variables to get hostname."); - - try - { - fqdn = (Environment.GetEnvironmentVariable("COMPUTERNAME") - ?? Environment.GetEnvironmentVariable("HOSTNAME")) - ?? Environment.GetEnvironmentVariable("HOST"); - - if (string.IsNullOrEmpty(fqdn)) - _logger.Error()?.Log("Failed to get hostname via environment variables."); - - return NormalizeHostName(fqdn); - } - catch (Exception e) - { - _logger.Error()?.LogException(e, "Failed to get hostname."); - } - - return null; - - static string NormalizeHostName(string hostName) => - string.IsNullOrEmpty(hostName) ? null : hostName.Trim().ToLower(); - } private void ParseContainerInfo(Api.System system, string reportedHostName) { diff --git a/test/Elastic.Apm.Tests.Utilities/TestAgentComponents.cs b/test/Elastic.Apm.Tests.Utilities/TestAgentComponents.cs index e1a8e1a7a..692d99caf 100644 --- a/test/Elastic.Apm.Tests.Utilities/TestAgentComponents.cs +++ b/test/Elastic.Apm.Tests.Utilities/TestAgentComponents.cs @@ -27,7 +27,10 @@ public TestAgentComponents( new FakeMetricsCollector(), currentExecutionSegmentsContainer, centralConfigurationFetcher ?? new NoopCentralConfigurationFetcher(), - apmServerInfo ?? MockApmServerInfo.Version710 + apmServerInfo ?? MockApmServerInfo.Version710, + null, + new TestHostNameDetector(configuration) + ) { } } diff --git a/test/Elastic.Apm.Tests.Utilities/TestHostNameDetector.cs b/test/Elastic.Apm.Tests.Utilities/TestHostNameDetector.cs new file mode 100644 index 000000000..0aff9bbb6 --- /dev/null +++ b/test/Elastic.Apm.Tests.Utilities/TestHostNameDetector.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm.Config; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Tests.Utilities; + +internal class TestHostNameDetector : IHostNameDetector +{ + private readonly string _hostName; + + public TestHostNameDetector(IConfiguration configuration) => + _hostName = configuration?.HostName ?? "MY_COMPUTER"; + + public TestHostNameDetector(string detectedHostName) => + _hostName = detectedHostName; + + public string GetDetectedHostName(IApmLogger logger) => _hostName; +} diff --git a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs index abb037297..af12f82d8 100644 --- a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs +++ b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs @@ -238,7 +238,7 @@ public void Dispose_stops_the_thread() using (var agent = new ApmAgent(new TestAgentComponents(LoggerBase, centralConfigurationFetcher: new CentralConfigurationFetcher(LoggerBase, configStore, service, handler), payloadSender: new PayloadSenderV2(LoggerBase, snapshot, service, - new SystemInfoHelper(LoggerBase).GetSystemInfo(null), MockApmServerInfo.Version710)))) + new SystemInfoHelper(LoggerBase).GetSystemInfo(null, new TestHostNameDetector("detected_hostname")), MockApmServerInfo.Version710)))) { lastCentralConfigurationFetcher = (CentralConfigurationFetcher)agent.CentralConfigurationFetcher; lastCentralConfigurationFetcher.IsRunning.Should().BeTrue(); @@ -283,7 +283,7 @@ public void Create_many_concurrent_instances(int numberOfAgentInstances) LoggerBase, snapshot, service, - new SystemInfoHelper(LoggerBase).GetSystemInfo(null), + new SystemInfoHelper(LoggerBase).GetSystemInfo(null, new TestHostNameDetector("detected_hostname")), MockApmServerInfo.Version710); var components = new TestAgentComponents(LoggerBase, centralConfigurationFetcher: centralConfigFetcher, payloadSender: payloadSender); diff --git a/test/Elastic.Apm.Tests/SerializationTests.cs b/test/Elastic.Apm.Tests/SerializationTests.cs index 9a76672c8..7ae39c0e1 100644 --- a/test/Elastic.Apm.Tests/SerializationTests.cs +++ b/test/Elastic.Apm.Tests/SerializationTests.cs @@ -451,7 +451,7 @@ public void System_Should_Serialize_ConfiguredHostName_And_DetectedHostName() var systemHelper = new SystemInfoHelper(new NoopLogger()); var hostName = "this_is_my_hostname"; - var system = systemHelper.GetSystemInfo(hostName); + var system = systemHelper.GetSystemInfo(hostName, new TestHostNameDetector("detected_hostname")); var json = _payloadItemSerializer.Serialize(system); var jObject = JObject.Parse(json); diff --git a/test/Elastic.Apm.Tests/SystemInfoHelperTests.cs b/test/Elastic.Apm.Tests/SystemInfoHelperTests.cs index a20f76d84..04e7cf1c6 100644 --- a/test/Elastic.Apm.Tests/SystemInfoHelperTests.cs +++ b/test/Elastic.Apm.Tests/SystemInfoHelperTests.cs @@ -27,7 +27,7 @@ public class SystemInfoHelperTests : IDisposable public void ParseSystemInfo_Should_Use_HostName_For_ConfiguredHostName() { var hostName = "This_is_my_host"; - var system = _systemInfoHelper.GetSystemInfo(hostName); + var system = _systemInfoHelper.GetSystemInfo(hostName, new TestHostNameDetector("detected_host_name")); #pragma warning disable 618 system.HostName.Should().Be(hostName); @@ -43,7 +43,7 @@ public void Feature_ContainerInfo_ShouldBeDisabled_OnAzure() var logger = new TestLogger(LogLevel.Trace); using (new AgentFeaturesProviderScope(new AzureFunctionsAgentFeatures(logger))) { - new SystemInfoHelper(logger).GetSystemInfo("bert"); + new SystemInfoHelper(logger).GetSystemInfo("bert", new TestHostNameDetector("detected_host_name")); // // The actual parsing (not happening) is hard to test currently. // Let's assert the log output that tells us that this part gets skipped. diff --git a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs index 26096cdb5..3612af656 100644 --- a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs @@ -629,7 +629,7 @@ private void FullFwAssertValid(Api.System system) { system.Should().NotBeNull(); - system.DetectedHostName.Should().Be(new SystemInfoHelper(LoggerBase).GetHostName()); + system.DetectedHostName.Should().Be(new HostNameDetector().GetDetectedHostName(LoggerBase)); #pragma warning disable 618 system.HostName.Should().Be(AgentConfig.HostName ?? system.DetectedHostName); #pragma warning restore 618 diff --git a/test/instrumentations/Elastic.Apm.Docker.Tests/ContainerIdCalculationTests.cs b/test/instrumentations/Elastic.Apm.Docker.Tests/ContainerIdCalculationTests.cs index 43e673b76..b58eed0b0 100644 --- a/test/instrumentations/Elastic.Apm.Docker.Tests/ContainerIdCalculationTests.cs +++ b/test/instrumentations/Elastic.Apm.Docker.Tests/ContainerIdCalculationTests.cs @@ -43,7 +43,7 @@ public void TestCGroupContent(string cGroupContent, string expectedContainerId) var noopLogger = new NoopLogger(); var systemInfoHelper = new TestSystemInfoHelper(noopLogger, cGroupContent); - var systemInfo = systemInfoHelper.GetSystemInfo(null); + var systemInfo = systemInfoHelper.GetSystemInfo(null, new TestHostNameDetector("detected_hostname")); systemInfo.Should().NotBeNull(); systemInfo.Container.Should().NotBeNull(); systemInfo.Container.Id.Should().Be(expectedContainerId); @@ -58,7 +58,7 @@ public void TestCGroupContentWithInvalidData() var noopLogger = new NoopLogger(); var systemInfoHelper = new TestSystemInfoHelper(noopLogger, "asdf:invalid-dockerid:243543"); - var systemInfo = systemInfoHelper.GetSystemInfo(null); + var systemInfo = systemInfoHelper.GetSystemInfo(null, new TestHostNameDetector("detected_hostname")); systemInfo.Container.Should().BeNull(); } From 40bf60158b33ed5ffa75ec20ee8a7254f4ea1e71 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 23 Jul 2024 10:23:03 +0200 Subject: [PATCH 08/77] Enable global file logging (#2371) This ensures we can specify file logging globally and uniformly. Greatly simplifying the debug-ability of the agent. The preferred way is to set any or all of the following: * `ELASTIC_OTEL_LOG_TARGETS` (to anything but `none`) * if only set to `stdout` no file will be created but global logging will kicking ) * `OTEL_DOTNET_AUTO_LOG_DIRECTORY` * `OTEL_LOG_LEVEL` * `trace` `debug` will enable global file logging if the other two variables are not set explicitly. This ensure we have one way to debug both our proprietary agent as well as the [Elastic OpenTelemetry Distribution](https://github.com/elastic/elastic-otel-dotnet). See: https://github.com/elastic/elastic-otel-dotnet/pull/129 For backwards compatible reasons the profiler variables are also supported: * `ELASTIC_APM_PROFILER_LOG` * `ELASTIC_APM_PROFILER_LOG_DIR` * `ELASTIC_APM_PROFILER_LOG_TARGETS` Globally setting * `ELASTIC_APM_LOG_LEVEL` and `ELASTIC_APM_LOG_DIRECTORY` is also supported but not preferred. Setting these in any of our supported deploy scenarios: * Manual instrumentation (nuget) * ASP.NET (classic) * ASP.NET core * NOTE: we now log to both the configured ILogger and our global logging. * Auto Instrumentation * Profiler * Startup hooks To keep support existing deploys this now always globally logs to file if only `ELASTIC_APM_STARTUP_HOOKS_LOGGING` is set as well. This further updates our docs for the profiler and troubleshooting to prefer `ELASTIC_OTEL_*` variables. The profiler is updated to read the same environment variables as managed code. --- docs/setup-auto-instrumentation.asciidoc | 15 +- docs/troubleshooting.asciidoc | 107 ++++----- src/Elastic.Apm/AgentComponents.cs | 49 +++-- .../FullFrameworkDefaultImplementations.cs | 2 +- src/Elastic.Apm/Config/ProfilerLogConfig.cs | 90 -------- .../Logging/GlobalLogConfiguration.cs | 206 ++++++++++++++++++ src/Elastic.Apm/Logging/LogLevel.cs | 95 ++++---- .../ApplicationBuilderExtensions.cs | 4 +- ...etCoreLogger.cs => ApmExtensionsLogger.cs} | 6 +- .../CompositeLogger.cs | 35 +++ .../Config/ApmConfiguration.cs | 2 +- .../HostBuilderExtensions.cs | 9 +- .../ServiceCollectionExtensions.cs | 31 ++- ...Elastic.Apm.Profiler.Managed.Loader.csproj | 5 + .../Logger.cs | 168 ++++++-------- .../Startup.cs | 2 +- .../CallTarget/Handlers/IntegrationOptions.cs | 4 +- .../Elastic.Apm.Profiler.Managed.csproj | 8 + .../Elastic.Apm.Profiler.Managed/Logger.cs | 144 ------------ .../Reflection/MethodBuilder.cs | 19 +- .../Reflection/ModuleLookup.cs | 3 +- .../elastic_apm_profiler/src/profiler/env.rs | 78 ++++--- .../Elastic.Apm.StartupHook.Loader.csproj | 6 - .../Elastic.Apm.StartupHook.Loader/Loader.cs | 25 +-- .../ElasticApmAgentStartupHook.csproj | 5 + .../StartupHookLogger.cs | 9 +- .../GlobalLogConfigurationPrecedenceTests.cs | 82 +++++++ .../Config/GlobalLogConfigurationTests.cs | 176 +++++++++++++++ .../Config/ProfilerLogConfigTests.cs | 86 -------- test/Elastic.Apm.Tests/LoggerTests.cs | 8 +- .../ApplicationBuilderExtensionLoggingTest.cs | 6 +- .../AspNetCoreBasicTests.cs | 1 + .../AspNetCoreLoggerTests.cs | 6 +- .../TransactionIgnoreUrlsTest.cs | 1 + .../BasicTests.cs | 3 +- .../ExcludeTests.cs | 7 +- .../ProfiledApplication.cs | 7 +- .../SatelliteAssemblyTests.cs | 3 +- 38 files changed, 849 insertions(+), 664 deletions(-) delete mode 100644 src/Elastic.Apm/Config/ProfilerLogConfig.cs create mode 100644 src/Elastic.Apm/Logging/GlobalLogConfiguration.cs rename src/integrations/Elastic.Apm.Extensions.Hosting/{NetCoreLogger.cs => ApmExtensionsLogger.cs} (84%) create mode 100644 src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs delete mode 100644 src/profiler/Elastic.Apm.Profiler.Managed/Logger.cs create mode 100644 test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs create mode 100644 test/Elastic.Apm.Tests/Config/GlobalLogConfigurationTests.cs delete mode 100644 test/Elastic.Apm.Tests/Config/ProfilerLogConfigTests.cs diff --git a/docs/setup-auto-instrumentation.asciidoc b/docs/setup-auto-instrumentation.asciidoc index 19ae58724..9bff72f74 100644 --- a/docs/setup-auto-instrumentation.asciidoc +++ b/docs/setup-auto-instrumentation.asciidoc @@ -398,7 +398,7 @@ A semi-colon separated list of APM service names to exclude from auto-instrument Values defined are checked against the value of <> environment variable. -`ELASTIC_APM_PROFILER_LOG` _(optional)_:: +`ELASTIC_OTEL_LOG_LEVEL` _(optional)_:: The log level at which the profiler should log. Valid values are @@ -409,11 +409,15 @@ The log level at which the profiler should log. Valid values are * error * none + The default value is `warn`. More verbose log levels like `trace` and `debug` can affect the runtime performance of profiler auto instrumentation, so are recommended _only_ for diagnostics purposes. -`ELASTIC_APM_PROFILER_LOG_DIR` _(optional)_:: +This takes precedence over the now deprecated `ELASTIC_APM_PROFILER_LOG` + + +`ELASTIC_OTEL_LOG_DIRECTORY` _(optional)_:: The directory in which to write profiler log files. If unset, defaults to @@ -424,6 +428,8 @@ If the default directory cannot be written to for some reason, the profiler will try to write log files to a `logs` directory in the home directory specified by `ELASTIC_APM_PROFILER_HOME` environment variable. +This takes precedence over the now deprecated `ELASTIC_APM_PROFILER_LOG_DIR` + [IMPORTANT] -- The user account under which the profiler process runs must have permission to @@ -432,7 +438,8 @@ on IIS, the https://learn.microsoft.com/en-us/iis/manage/configuring-security/ap has write permissions in the target directory. -- -`ELASTIC_APM_PROFILER_LOG_TARGETS` _(optional)_:: + +`ELASTIC_OTEL_LOG_TARGETS` _(optional)_:: A semi-colon separated list of targets for profiler logs. Valid values are @@ -441,3 +448,5 @@ A semi-colon separated list of targets for profiler logs. Valid values are The default value is `file`, which logs to the directory specified by `ELASTIC_APM_PROFILER_LOG_DIR` environment variable. + +This takes precedence over the now deprecated `ELASTIC_APM_PROFILER_LOG_TARGETS` \ No newline at end of file diff --git a/docs/troubleshooting.asciidoc b/docs/troubleshooting.asciidoc index ba23c0943..2e9d962f0 100644 --- a/docs/troubleshooting.asciidoc +++ b/docs/troubleshooting.asciidoc @@ -32,7 +32,58 @@ If you don't see anything suspicious in the agent logs (no warning or error), it [[collect-agent-logs]] === Collecting agent logs -The way to collect logs depends on the setup of your application. +[float] +[[collect-logs-globally]] +==== Enable global file logging. + +The easiest way to get debug information from the Agent, regardless of the way it's run, is to enable global file logging. + +Specifying at least one of the following environment variables will ensure the agent logs to a file + +`OTEL_LOG_LEVEL` _(optional)_:: + +The log level at which the profiler should log. Valid values are + +* trace +* debug +* info +* warn +* error +* none + + +The default value is `warn`. More verbose log levels like `trace` and `debug` can +affect the runtime performance of profiler auto instrumentation, so are recommended +_only_ for diagnostics purposes. + +NOTE: if `ELASTIC_OTEL_LOG_TARGETS` is not explicitly set to include `file` global file logging will only +be enabled when configured with `trace` or `debug`. + +`OTEL_DOTNET_AUTO_LOG_DIRECTORY` _(optional)_:: + +The directory in which to write log files. If unset, defaults to + +* `%PROGRAMDATA%\elastic\apm-agent-dotnet\logs` on Windows +* `/var/log/elastic/apm-agent-dotnet` on Linux + + +[IMPORTANT] +-- +The user account under which the profiler process runs must have permission to +write to the destination log directory. Specifically, ensure that when running +on IIS, the https://learn.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities[AppPool identity] +has write permissions in the target directory. +-- + +`ELASTIC_OTEL_LOG_TARGETS` _(optional)_:: + +A semi-colon separated list of targets for profiler logs. Valid values are + +* file +* stdout + +The default value is `file` if `OTEL_DOTNET_AUTO_LOG_DIRECTORY` is set or `OTEL_LOG_LEVEL` is set to `trace` or `debug`. + [float] [[collect-logs-core]] @@ -62,49 +113,9 @@ For example, the following configuration in `appsettings.json` limits APM agents ---- <1> Control the verbosity of the agent logs by setting the log level for the `Elastic.Apm` category -[float] -[[collect-logs-classic]] -==== ASP.NET Classic - -ASP.NET (classic) does not have a predefined logging system. By default, the agent is configured to -emit log messages to a -https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracesource[`System.Diagnostics.TraceSource`] -with the source name `"Elastic.Apm"`. The TraceSource adheres to the log levels defined in the -APM agent configuration. Typically, you will configure a preferred log level using an application setting in `web.config`. - -[IMPORTANT] --- -System.Diagnostics.TraceSource requires the https://docs.microsoft.com/en-us/dotnet/framework/debug-trace-profile/how-to-compile-conditionally-with-trace-and-debug[`TRACE` compiler directive to be specified], which is specified -by default for both Debug and Release build configurations. --- - -https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracelistener[TraceListeners] -can be configured to monitor log messages for the trace source, using the https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/trace-debug/system-diagnostics-element[``] section of -web.config. For example, the following web.config section writes Elastic.Apm log messages to a file -named my_log_file.log: - -[source,xml] ----- - - - - - <1> - - - - - - - ----- -<1> Define listeners under a source with name `"Elastic.Apm"` to capture agent logs - [float] [[collect-logs-class-other-logging-systems]] -===== Other logging systems +==== Other logging systems If you have a logging system in place such as https://nlog-project.org/[NLog], https://serilog.net/[Serilog], or similar, you can direct the agent logs into your logging system by creating an adapter between @@ -242,17 +253,7 @@ The agent is only supported on IIS7 and higher where the `Integrated Pipeline Mo [[startup-hook-failure]] === Startup hooks failure -If the <> integration throws an exception, additional detail can be obtained by -setting the `ELASTIC_APM_STARTUP_HOOKS_LOGGING` environment variable before starting the application - -[source,sh] ----- -set ELASTIC_APM_STARTUP_HOOKS_LOGGING=1 ----- - -and then running the application in a context where the environment variable will be visible. In setting this value, -an `ElasticApmAgentStartupHook.log` file is written to the directory containing the startup hook assembly, in addition to -writing to standard output. +If the <> integration throws an exception, additional detail can be obtained through <>. [float] [[agent-overhead]] diff --git a/src/Elastic.Apm/AgentComponents.cs b/src/Elastic.Apm/AgentComponents.cs index 917a747d7..ded9d166f 100644 --- a/src/Elastic.Apm/AgentComponents.cs +++ b/src/Elastic.Apm/AgentComponents.cs @@ -52,7 +52,7 @@ internal AgentComponents( var config = CreateConfiguration(logger, configurationReader); hostNameDetector ??= new HostNameDetector(); - Logger = logger ?? CheckForProfilerLogger(DefaultLogger(null, configurationReader), config.LogLevel); + Logger = logger ?? GetGlobalLogger(DefaultLogger(null, configurationReader), config.LogLevel); ConfigurationStore = new ConfigurationStore(new RuntimeConfigurationSnapshot(config), Logger); Service = Service.GetDefaultService(config, Logger); @@ -199,38 +199,39 @@ private static IConfigurationReader CreateConfiguration(IApmLogger logger, IConf #endif } - - // - // This is the hooking point that checks for the existence of profiler-related - // logging settings. - // If no agent logging is configured but we detect profiler logging settings, those - // will be honoured. - // The finer-grained log-level (agent vs profiler) will be used. - // This has the benefit that users will also get agent logs in addition to profiler-only - // logs. - // - internal static IApmLogger CheckForProfilerLogger(IApmLogger fallbackLogger, LogLevel agentLogLevel, IDictionary environmentVariables = null) + /// + /// This ensures agents will respect externally provided loggers. + /// If the agent is started as part of profiling it should adhere to profiling configuration + /// If file logging environment variables are set we should always log to that location + /// + /// + /// + /// + /// + internal static IApmLogger GetGlobalLogger(IApmLogger fallbackLogger, LogLevel agentLogLevel, IDictionary environmentVariables = null) { try { - var profilerLogConfig = ProfilerLogConfig.Check(environmentVariables); - if (profilerLogConfig.IsActive) + var fileLogConfig = GlobalLogConfiguration.FromEnvironment(environmentVariables); + if (!fileLogConfig.IsActive) { - var effectiveLogLevel = LogLevelUtils.GetFinest(agentLogLevel, profilerLogConfig.LogLevel); + fallbackLogger.Info()?.Log("No system wide logging configured, defaulting to fallback logger"); + return fallbackLogger; + } - if ((profilerLogConfig.LogTargets & ProfilerLogTarget.File) == ProfilerLogTarget.File) - TraceLogger.TraceSource.Listeners.Add(new TextWriterTraceListener(profilerLogConfig.LogFilePath)); - if ((profilerLogConfig.LogTargets & ProfilerLogTarget.StdOut) == ProfilerLogTarget.StdOut) - TraceLogger.TraceSource.Listeners.Add(new TextWriterTraceListener(Console.Out)); + var effectiveLogLevel = LogLevelUtils.GetFinest(agentLogLevel, fileLogConfig.LogLevel); + if ((fileLogConfig.LogTargets & GlobalLogTarget.File) == GlobalLogTarget.File) + TraceLogger.TraceSource.Listeners.Add(new TextWriterTraceListener(fileLogConfig.AgentLogFilePath)); + if ((fileLogConfig.LogTargets & GlobalLogTarget.StdOut) == GlobalLogTarget.StdOut) + TraceLogger.TraceSource.Listeners.Add(new TextWriterTraceListener(Console.Out)); - var logger = new TraceLogger(effectiveLogLevel); - logger.Info()?.Log($"{nameof(ProfilerLogConfig)} - {profilerLogConfig}"); - return logger; - } + var logger = new TraceLogger(effectiveLogLevel); + logger.Info()?.Log($"{nameof(fileLogConfig)} - {fileLogConfig}"); + return logger; } catch (Exception e) { - fallbackLogger.Warning()?.LogException(e, "Error in CheckForProfilerLogger"); + fallbackLogger.Warning()?.LogException(e, "Error in GetGlobalLogger"); } return fallbackLogger; } diff --git a/src/Elastic.Apm/Config/Net4FullFramework/FullFrameworkDefaultImplementations.cs b/src/Elastic.Apm/Config/Net4FullFramework/FullFrameworkDefaultImplementations.cs index 888d652cb..73b79054e 100644 --- a/src/Elastic.Apm/Config/Net4FullFramework/FullFrameworkDefaultImplementations.cs +++ b/src/Elastic.Apm/Config/Net4FullFramework/FullFrameworkDefaultImplementations.cs @@ -22,7 +22,7 @@ internal static IApmLogger CreateDefaultLogger(LogLevel? configuredDefault) if (!string.IsNullOrEmpty(logLevel)) Enum.TryParse(logLevel, true, out level); - return AgentComponents.CheckForProfilerLogger(new TraceLogger(level), level); + return AgentComponents.GetGlobalLogger(new TraceLogger(level), level); } /// diff --git a/src/Elastic.Apm/Config/ProfilerLogConfig.cs b/src/Elastic.Apm/Config/ProfilerLogConfig.cs deleted file mode 100644 index 5af70ca99..000000000 --- a/src/Elastic.Apm/Config/ProfilerLogConfig.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using Elastic.Apm.Logging; - -namespace Elastic.Apm.Config -{ - internal readonly struct ProfilerLogConfig - { - private ProfilerLogConfig(bool isActive, LogLevel logLevel, ProfilerLogTarget logTarget, string logFilePath) : this() - { - IsActive = isActive; - LogLevel = logLevel; - LogTargets = logTarget; - LogFilePath = logFilePath; - } - - internal bool IsActive { get; } - internal ProfilerLogTarget LogTargets { get; } - internal string LogFilePath { get; } - internal LogLevel LogLevel { get; } - - public override string ToString() => - $"IsActive: '{IsActive}', LogLevel: '{LogLevel}', LogTargets: '{LogTargets}', LogFilePath: '{LogFilePath}'"; - - internal static string GetDefaultProfilerLogDirectory() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Path.Combine(Environment.GetEnvironmentVariable("PROGRAMDATA"), "elastic", "apm-agent-dotnet", "logs") - : "/var/log/elastic/apm-agent-dotnet"; - - internal static ProfilerLogConfig Check(IDictionary environmentVariables = null) - { - environmentVariables ??= Environment.GetEnvironmentVariables(); - - string GetSafeEnvironmentVariable(string key) - { - var value = environmentVariables.Contains(key) ? environmentVariables[key]?.ToString() : null; - return value ?? string.Empty; - } - - var v = GetSafeEnvironmentVariable("ELASTIC_APM_PROFILER_LOG"); - - var isActive = !string.IsNullOrEmpty(v); - - var logLevel = v.ToLowerInvariant() switch - { - "trace" => LogLevel.Trace, - "debug" => LogLevel.Debug, - "info" => LogLevel.Information, - "warn" => LogLevel.Warning, - "error" => LogLevel.Error, - "none" => LogLevel.None, - _ => LogLevel.Warning, - }; - - var logFilePath = GetSafeEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_DIR"); - if (string.IsNullOrEmpty(logFilePath)) - logFilePath = GetDefaultProfilerLogDirectory(); - var process = Process.GetCurrentProcess(); - var logFileName = Path.Combine(logFilePath, $"{process.ProcessName}_{process.Id}_{Environment.TickCount}.agent.log"); - - var logTargets = ProfilerLogTarget.None; - foreach (var target in GetSafeEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_TARGETS").Split(';')) - { - if (target.Equals("stdout", StringComparison.InvariantCultureIgnoreCase)) - logTargets |= ProfilerLogTarget.StdOut; - else if (target.Equals("file", StringComparison.InvariantCultureIgnoreCase)) - logTargets |= ProfilerLogTarget.File; - } - - if (logTargets == ProfilerLogTarget.None) - logTargets = ProfilerLogTarget.File; - - return new(isActive, logLevel, logTargets, logFileName); - } - } - - [Flags] - internal enum ProfilerLogTarget - { - None = 0, - File = 1, - StdOut = 2 - } -} diff --git a/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs b/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs new file mode 100644 index 000000000..dfd1ac6b3 --- /dev/null +++ b/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs @@ -0,0 +1,206 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Linq; + +#if PROFILER_MANAGED_LOADER +using static Elastic.Apm.Profiler.Managed.Loader.LogEnvironmentVariables; +namespace Elastic.Apm.Profiler.Managed.Loader; +#elif PROFILER_MANAGED +using static Elastic.Apm.Profiler.Managed.LogEnvironmentVariables; +namespace Elastic.Apm.Profiler.Managed; +#elif STARTUP_HOOKS +using static ElasticApmStartupHook.LogEnvironmentVariables; +namespace ElasticApmStartupHook; +#else +using static Elastic.Apm.Logging.LogEnvironmentVariables; + +namespace Elastic.Apm.Logging; +#endif + +internal class EnvironmentLoggingConfiguration(IDictionary environmentVariables = null) +{ + public IDictionary EnvironmentVariables { get; } = environmentVariables ?? Environment.GetEnvironmentVariables(); + +public string GetSafeEnvironmentVariable(string key) +{ + var value = EnvironmentVariables.Contains(key) ? EnvironmentVariables[key]?.ToString() : null; + return value ?? string.Empty; +} + +public LogLevel? GetLogLevel(params string[] keys) +{ + var level = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .Select(v => v.ToLowerInvariant() switch + { + "trace" => LogLevel.Trace, + "debug" => LogLevel.Debug, + "info" => LogLevel.Information, + "warn" => LogLevel.Warning, + "error" => LogLevel.Error, + "none" => LogLevel.None, + _ => null + }) + .FirstOrDefault(l => l != null); + return level; +} + +public string GetLogDirectory(params string[] keys) +{ + var path = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .FirstOrDefault(p => !string.IsNullOrEmpty(p)); + + return path; +} + +public bool AnyConfigured(params string[] keys) => + keys + .Select(k => GetSafeEnvironmentVariable(k)) + .Any(p => !string.IsNullOrEmpty(p)); + +public GlobalLogTarget? ParseLogTargets(params string[] keys) +{ + var targets = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .FirstOrDefault(p => !string.IsNullOrEmpty(p)); + if (string.IsNullOrWhiteSpace(targets)) + return null; + + var logTargets = GlobalLogTarget.None; + var found = false; + + foreach (var target in targets.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (IsSet(target, "stdout")) + logTargets |= GlobalLogTarget.StdOut; + else if (IsSet(target, "file")) + logTargets |= GlobalLogTarget.File; + else if (IsSet(target, "none")) + logTargets |= GlobalLogTarget.None; + } + return !found ? null : logTargets; + + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, StringComparison.InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } +} + +internal static string GetDefaultLogDirectory() => + Environment.OSVersion.Platform == PlatformID.Win32NT + ? Path.Combine(Environment.GetEnvironmentVariable("PROGRAMDATA")!, "elastic", "apm-agent-dotnet", "logs") + : "/var/log/elastic/apm-agent-dotnet"; +} + +[Flags] +internal enum GlobalLogTarget +{ + None = 0, + File = 1, + StdOut = 2 +} + +public static class LogEnvironmentVariables +{ + // ReSharper disable once InconsistentNaming + public const string OTEL_LOG_LEVEL = nameof(OTEL_LOG_LEVEL); + public const string OTEL_DOTNET_AUTO_LOG_DIRECTORY = nameof(OTEL_DOTNET_AUTO_LOG_DIRECTORY); + public const string ELASTIC_OTEL_LOG_TARGETS = nameof(ELASTIC_OTEL_LOG_TARGETS); + + public const string ELASTIC_APM_LOG_LEVEL = nameof(ELASTIC_APM_LOG_LEVEL); + public const string ELASTIC_APM_LOG_DIRECTORY = nameof(ELASTIC_APM_LOG_DIRECTORY); + + + // profiler logs are deprecated in favor of ELASTIC_OTEL_* + public const string ELASTIC_APM_PROFILER_LOG = nameof(ELASTIC_APM_PROFILER_LOG); + public const string ELASTIC_APM_PROFILER_LOG_DIR = nameof(ELASTIC_APM_PROFILER_LOG_DIR); + public const string ELASTIC_APM_PROFILER_LOG_TARGETS = nameof(ELASTIC_APM_PROFILER_LOG_TARGETS); + + // deprected startup hooks logging configuration, we still listen to it to enable logging + public const string ELASTIC_APM_STARTUP_HOOKS_LOGGING = nameof(ELASTIC_APM_STARTUP_HOOKS_LOGGING); + + // ReSharper enable once InconsistentNaming +} + +internal readonly struct GlobalLogConfiguration +{ + private GlobalLogConfiguration(bool isActive, LogLevel logLevel, GlobalLogTarget logTarget, string logFileDirectory, string logFilePrefix) : + this() + { + IsActive = isActive; + LogLevel = logLevel; + LogTargets = logTarget; + LogFileDirectory = logFileDirectory; + LogFilePrefix = logFilePrefix; + + AgentLogFilePath = CreateLogFileName(); + } + + internal bool IsActive { get; } + internal string AgentLogFilePath { get; } + internal LogLevel LogLevel { get; } + internal GlobalLogTarget LogTargets { get; } + + internal string LogFileDirectory { get; } + internal string LogFilePrefix { get; } + + public override string ToString() => $"IsActive: '{IsActive}', Targets: '{LogTargets}', Level: '{LogLevel}', FilePath: '{AgentLogFilePath}'"; + + internal static GlobalLogConfiguration FromEnvironment(IDictionary environmentVariables = null) + { + var config = new EnvironmentLoggingConfiguration(environmentVariables); + var otelLogLevel = config.GetLogLevel(OTEL_LOG_LEVEL); + var profilerLogLevel = config.GetLogLevel(ELASTIC_APM_PROFILER_LOG); + var apmLogLevel = config.GetLogLevel(ELASTIC_APM_LOG_LEVEL); + + var logLevel = otelLogLevel ?? profilerLogLevel ?? apmLogLevel; + + var logFileDirectory = config.GetLogDirectory(OTEL_DOTNET_AUTO_LOG_DIRECTORY, ELASTIC_APM_PROFILER_LOG_DIR, ELASTIC_APM_LOG_DIRECTORY); + var logFilePrefix = GetLogFilePrefix(); + var logTarget = config.ParseLogTargets(ELASTIC_OTEL_LOG_TARGETS, ELASTIC_APM_PROFILER_LOG_TARGETS); + + //The presence of some variables enable file logging for historical purposes + var isActive = config.AnyConfigured(ELASTIC_APM_STARTUP_HOOKS_LOGGING); + var activeFromLogging = + otelLogLevel is <= LogLevel.Debug + || apmLogLevel is <= LogLevel.Debug + || profilerLogLevel.HasValue; + if (activeFromLogging || !string.IsNullOrWhiteSpace(logFileDirectory) || logTarget.HasValue) + { + isActive = true; + if (logLevel is LogLevel.None) + isActive = false; + else if (logTarget is GlobalLogTarget.None) + isActive = false; + } + + // now that we know what's actively configured, assign defaults + logFileDirectory ??= EnvironmentLoggingConfiguration.GetDefaultLogDirectory(); + var level = logLevel ?? LogLevel.Warning; + + var target = logTarget ?? (isActive ? GlobalLogTarget.File : GlobalLogTarget.None); + return new(isActive, level, target, logFileDirectory, logFilePrefix); + } + + private static string GetLogFilePrefix() + { + var process = Process.GetCurrentProcess(); + return $"{process.ProcessName}_{process.Id}_{Environment.TickCount}"; + } + + public string CreateLogFileName(string applicationName = "agent") + { + var logFileName = Path.Combine(LogFileDirectory, $"{LogFilePrefix}.{applicationName}.log"); + return logFileName; + } +} diff --git a/src/Elastic.Apm/Logging/LogLevel.cs b/src/Elastic.Apm/Logging/LogLevel.cs index f0d012d3b..05bab8a8f 100644 --- a/src/Elastic.Apm/Logging/LogLevel.cs +++ b/src/Elastic.Apm/Logging/LogLevel.cs @@ -2,49 +2,56 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Apm.Logging +#if PROFILER_MANAGED_LOADER +namespace Elastic.Apm.Profiler.Managed.Loader; +#elif PROFILER_MANAGED +namespace Elastic.Apm.Profiler.Managed; +#elif STARTUP_HOOKS +namespace ElasticApmStartupHook; +#else +namespace Elastic.Apm.Logging; +#endif + +// https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=aspnetcore-2.2 +public enum LogLevel { - // https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=aspnetcore-2.2 - public enum LogLevel - { - /// - /// Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are - /// disabled by default and should never be enabled in a production environment. - /// - Trace = 0, - - /// - /// Logs that are used for interactive investigation during development. These logs should primarily contain information - /// useful for debugging and have no long-term value. - /// - Debug = 1, - - /// - /// Logs that track the general flow of the application. These logs should have long-term value. - /// - Information = 2, - - /// - /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application - /// execution to stop. - /// - Warning = 3, - - /// - /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure - /// in the current activity, not an application-wide failure. - /// - Error = 4, - - /// - /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate - /// attention. - /// - Critical = 5, - - /// - /// Not used for writing log messages. Specifies that a logging category should not write any messages. - /// - None = 6 - } + /// + /// Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are + /// disabled by default and should never be enabled in a production environment. + /// + Trace = 0, + + /// + /// Logs that are used for interactive investigation during development. These logs should primarily contain information + /// useful for debugging and have no long-term value. + /// + Debug = 1, + + /// + /// Logs that track the general flow of the application. These logs should have long-term value. + /// + Information = 2, + + /// + /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application + /// execution to stop. + /// + Warning = 3, + + /// + /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure + /// in the current activity, not an application-wide failure. + /// + Error = 4, + + /// + /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate + /// attention. + /// + Critical = 5, + + /// + /// Not used for writing log messages. Specifies that a logging category should not write any messages. + /// + None = 6 } diff --git a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs index 0a260f0d8..1efbfe841 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs @@ -52,7 +52,7 @@ public static IApplicationBuilder UseElasticApm( params IDiagnosticsSubscriber[] subscribers ) { - var logger = NetCoreLogger.GetApmLogger(builder.ApplicationServices); + var logger = ApmExtensionsLogger.GetApmLogger(builder.ApplicationServices); var configReader = configuration == null ? new EnvironmentConfiguration(logger) @@ -70,7 +70,7 @@ params IDiagnosticsSubscriber[] subscribers internal static IApplicationBuilder UseElasticApm( this IApplicationBuilder builder, ApmAgent agent, - IApmLogger logger, + Logging.IApmLogger logger, params IDiagnosticsSubscriber[] subscribers ) { diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/ApmExtensionsLogger.cs similarity index 84% rename from src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs rename to src/integrations/Elastic.Apm.Extensions.Hosting/ApmExtensionsLogger.cs index f806f3499..48d6dd422 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ApmExtensionsLogger.cs @@ -9,11 +9,11 @@ namespace Elastic.Apm.Extensions.Hosting; -internal sealed class NetCoreLogger : IApmLogger +internal sealed class ApmExtensionsLogger : IApmLogger { private readonly ILogger _logger; - public NetCoreLogger(ILoggerFactory loggerFactory) => _logger = loggerFactory?.CreateLogger("Elastic.Apm") ?? throw new ArgumentNullException(nameof(loggerFactory)); + public ApmExtensionsLogger(ILoggerFactory loggerFactory) => _logger = loggerFactory?.CreateLogger("Elastic.Apm") ?? throw new ArgumentNullException(nameof(loggerFactory)); public bool IsEnabled(LogLevel level) => _logger.IsEnabled(Convert(level)); @@ -34,6 +34,6 @@ private static Microsoft.Extensions.Logging.LogLevel Convert(LogLevel logLevel) internal static IApmLogger GetApmLogger(IServiceProvider serviceProvider) => serviceProvider.GetService(typeof(ILoggerFactory)) is ILoggerFactory loggerFactory - ? new NetCoreLogger(loggerFactory) + ? new ApmExtensionsLogger(loggerFactory) : ConsoleLogger.Instance; } diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs new file mode 100644 index 000000000..e7498dc98 --- /dev/null +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using Elastic.Apm.Logging; +using LogLevel = Elastic.Apm.Logging.LogLevel; + +namespace Elastic.Apm.Extensions.Hosting; + +internal sealed class CompositeLogger(TraceLogger traceLogger, IApmLogger logger) : IDisposable , IApmLogger +{ + public TraceLogger TraceLogger { get; } = traceLogger; +public IApmLogger ApmLogger { get; } = logger; + +private bool _isDisposed; + +public void Dispose() => _isDisposed = true; + +public void Log(LogLevel level, TState state, Exception e, Func formatter) +{ + if (_isDisposed) + return; + + if (TraceLogger.IsEnabled(level)) + TraceLogger.Log(level, state, e, formatter); + + if (ApmLogger.IsEnabled(level)) + ApmLogger.Log(level, state, e, formatter); +} + +public bool IsEnabled(LogLevel logLevel) => ApmLogger.IsEnabled(logLevel) || TraceLogger.IsEnabled(logLevel); + + +} diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/Config/ApmConfiguration.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/Config/ApmConfiguration.cs index b07095571..4105fa587 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/Config/ApmConfiguration.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/Config/ApmConfiguration.cs @@ -33,7 +33,7 @@ internal class ApmConfiguration : FallbackToEnvironmentConfigurationBase { private const string ThisClassName = nameof(ApmConfiguration); - public ApmConfiguration(IConfiguration configuration, IApmLogger logger, string defaultEnvironmentName) + public ApmConfiguration(IConfiguration configuration, Apm.Logging.IApmLogger logger, string defaultEnvironmentName) : base(logger, new ConfigurationDefaults { EnvironmentName = defaultEnvironmentName, DebugName = ThisClassName }, new ConfigurationKeyValueProvider(configuration)) => diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs index d33a62d73..91f97818b 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using LogLevel = Elastic.Apm.Logging.LogLevel; namespace Elastic.Apm.Extensions.Hosting { @@ -54,7 +55,13 @@ public static IHostBuilder UseElasticApm(this IHostBuilder builder, params IDiag //If the static agent doesn't exist, we create one here. If there is already 1 agent created, we reuse it. if (!Agent.IsConfigured) { - services.AddSingleton(); + services.AddSingleton(sp => + { + var netCoreLogger = ApmExtensionsLogger.GetApmLogger(sp); + var globalLogger = AgentComponents.GetGlobalLogger(netCoreLogger, LogLevel.Error); + var logger = globalLogger is TraceLogger g ? new CompositeLogger(g, netCoreLogger) : netCoreLogger; + return logger; + }); services.AddSingleton(sp => new ApmConfiguration(ctx.Configuration, sp.GetService(), GetHostingEnvironmentName(ctx, sp.GetService()))); } diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs index 1923e42e2..2931fe1a3 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs @@ -12,10 +12,8 @@ using Elastic.Apm.Logging; using Elastic.Apm.NetCoreAll; using Elastic.Apm.Report; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; namespace Microsoft.Extensions.DependencyInjection; @@ -49,28 +47,25 @@ public static IServiceCollection AddElasticApm(this IServiceCollection services, if (agentConfigured) return Agent.Instance; - var logger = NetCoreLogger.GetApmLogger(sp); - var environmentName = GetEnvironmentName(sp); - - if (environmentName is null) - { - logger?.Warning()?.Log("Failed to retrieve hosting environment name"); - environmentName = "Undetermined"; - } - + var netCoreLogger = ApmExtensionsLogger.GetApmLogger(sp); var configuration = sp.GetService(); + var environmentName = GetDefaultEnvironmentName(sp); IConfigurationReader configurationReader = configuration is null - ? new EnvironmentConfiguration(logger) - : new ApmConfiguration(configuration, logger, environmentName); + ? new EnvironmentConfiguration(netCoreLogger) + : new ApmConfiguration(configuration, netCoreLogger, environmentName ?? "Undetermined"); + + var globalLogger = AgentComponents.GetGlobalLogger(netCoreLogger, configurationReader.LogLevel); + + var logger = globalLogger is TraceLogger g ? new CompositeLogger(g, netCoreLogger) : netCoreLogger; + + if (environmentName is null) + logger?.Warning()?.Log("Failed to retrieve default hosting environment name"); // This may be null, which is fine var payloadSender = sp.GetService(); - var components = agentConfigured - ? Agent.Components - : new AgentComponents(logger, configurationReader, payloadSender); - + var components = new AgentComponents(logger, configurationReader, payloadSender); HostBuilderExtensions.UpdateServiceInformation(components.Service); Agent.Setup(components); @@ -110,7 +105,7 @@ public static IServiceCollection AddElasticApm(this IServiceCollection services, return services; } - private static string GetEnvironmentName(IServiceProvider serviceProvider) => + private static string GetDefaultEnvironmentName(IServiceProvider serviceProvider) => #if NET6_0_OR_GREATER (serviceProvider.GetService(typeof(IHostEnvironment)) as IHostEnvironment)?.EnvironmentName; // This is preferred since 3.0 #else diff --git a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj index e826eeee6..373ae5b73 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj +++ b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj @@ -5,11 +5,16 @@ net462;netcoreapp2.0 false false + $(DefineConstants);PROFILER_MANAGED_LOADER portable + + + + diff --git a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Logger.cs b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Logger.cs index 06f351bca..b498caca6 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Logger.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Logger.cs @@ -5,134 +5,92 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Text; -namespace Elastic.Apm.Profiler.Managed.Loader +#if PROFILER_MANAGED +namespace Elastic.Apm.Profiler.Managed; +#else +namespace Elastic.Apm.Profiler.Managed.Loader; +#endif + +internal static class Logger { - // match the log levels of the profiler logger - internal enum LogLevel + static Logger() { - Trace = 0, - Debug = 1, - Info = 2, - Warn = 3, - Error = 4, - Off = 5, + var config = GlobalLogConfiguration.FromEnvironment(Environment.GetEnvironmentVariables()); + + Level = config.LogLevel; + IsActive = config.IsActive; +#if PROFILER_MANAGED + LogFile = config.CreateLogFileName("profiler_managed"); +#else + LogFile = config.CreateLogFileName("profiler_managed_loader"); +#endif + Levels = new Dictionary + { + [LogLevel.None] = "OFF ", + [LogLevel.Error] = "ERROR", + [LogLevel.Warning] = "WARN ", + [LogLevel.Information] = "INFO ", + [LogLevel.Debug] = "DEBUG", + [LogLevel.Critical] = "CRITI", + [LogLevel.Trace] = "TRACE", + }; } - internal static class Logger - { - static Logger() - { - Level = GetLogLevel(LogLevel.Warn); - var logDirectory = GetLogDirectory(); - LogFile = GetLogFile(logDirectory); - Levels = new Dictionary - { - [LogLevel.Off] = "OFF ", - [LogLevel.Error] = "ERROR", - [LogLevel.Warn] = "WARN ", - [LogLevel.Info] = "INFO ", - [LogLevel.Debug] = "DEBUG", - [LogLevel.Trace] = "TRACE", - }; - } + private static readonly bool IsActive; + private static readonly LogLevel Level; + private static readonly string LogFile; + private static readonly Dictionary Levels; - private static readonly LogLevel Level; - private static readonly string LogFile; - private static readonly Dictionary Levels; + public static void Warn(string message, params object[] args) => Log(LogLevel.Warning, message, args); - public static void Log(LogLevel level, Exception exception, string message, params object[] args) - { - if (Level > level) - return; + public static void Debug(string message, params object[] args) => Log(LogLevel.Debug, message, args); - Log(level, $"{message}{Environment.NewLine}{exception}", args); - } + public static void Error(Exception exception, string message, params object[] args) => Log(LogLevel.Error, exception, message, args); - public static void Log(LogLevel level, string message, params object[] args) - { - if (Level > level) - return; + public static void Error(string message, params object[] args) => Log(LogLevel.Error, message, args); - try - { - if (LogFile != null) - { - try - { - using (var stream = File.Open(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read)) - using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) - { - writer.Write($"[{DateTimeOffset.Now:O}] [{Levels[level]}] "); - writer.WriteLine(message, args); - writer.Flush(); - stream.Flush(true); - } - - return; - } - catch - { - // ignore - } - } - - Console.Error.WriteLine($"[{DateTimeOffset.Now:O}] [{Levels[level]}] {message}", args); - } - catch - { - // ignore - } - } - - private static string GetLogFile(string logDirectory) - { - if (logDirectory is null) - return null; + public static void Log(LogLevel level, Exception exception, string message, params object[] args) + { + if (!IsActive || Level > level) + return; - var process = Process.GetCurrentProcess(); - return Path.Combine(logDirectory, $"Elastic.Apm.Profiler.Managed.Loader_{process.ProcessName}_{process.Id}.log"); - } + Log(level, $"{message}{Environment.NewLine}{exception}", args); + } - private static LogLevel GetLogLevel(LogLevel defaultLevel) - { - var level = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG"); - if (string.IsNullOrEmpty(level)) - return defaultLevel; + public static void Log(LogLevel level, string message, params object[] args) + { + if (!IsActive || Level > level) + return; - return Enum.TryParse(level, true, out var parsedLevel) - ? parsedLevel - : defaultLevel; - } + if (string.IsNullOrWhiteSpace(LogFile)) + return; - private static string GetLogDirectory() + try { try { - var logDirectory = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_DIR"); - if (string.IsNullOrEmpty(logDirectory)) - { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - var programData = Environment.GetEnvironmentVariable("PROGRAMDATA"); - logDirectory = !string.IsNullOrEmpty(programData) - ? Path.Combine(programData, "elastic", "apm-agent-dotnet", "logs") - : "."; - } - else - logDirectory = "/var/log/elastic/apm-agent-dotnet"; - } - - Directory.CreateDirectory(logDirectory); - return logDirectory; + using var stream = File.Open(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read); + using var writer = new StreamWriter(stream, new UTF8Encoding(false)); + writer.Write($"[{DateTimeOffset.Now:O}] [{Levels[level]}] "); + writer.WriteLine(message, args); + writer.Flush(); + stream.Flush(true); + + return; } catch { - return null; + // ignore } + + Console.Error.WriteLine($"[{DateTimeOffset.Now:O}] [{Levels[level]}] {message}", args); + } + catch + { + // ignore } } } diff --git a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Startup.cs b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Startup.cs index e3cf22fff..1c1e47713 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Startup.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Startup.cs @@ -16,7 +16,7 @@ public partial class Startup { static Startup() { - Logger.Log(LogLevel.Info, "Elastic.Apm.Profiler.Managed.Loader.Startup: Invoked "); + Logger.Log(LogLevel.Information, "Elastic.Apm.Profiler.Managed.Loader.Startup: Invoked "); Directory = ResolveDirectory(); try diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs b/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs index f80f0f42f..cea0679aa 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs @@ -32,14 +32,14 @@ internal static void LogException(Exception exception, string message = null) if (exception is DuckTypeException) { - Logger.Log(LogLevel.Warn, "DuckTypeException has been detected, the integration <{0}, {1}> will be disabled.", + Logger.Warn("DuckTypeException has been detected, the integration <{0}, {1}> will be disabled.", typeof(TIntegration).FullName, typeof(TTarget).FullName); _disableIntegration = true; } else if (exception is CallTargetInvokerException) { - Logger.Log(LogLevel.Warn, "CallTargetInvokerException has been detected, the integration <{0}, {1}> will be disabled.", + Logger.Warn("CallTargetInvokerException has been detected, the integration <{0}, {1}> will be disabled.", typeof(TIntegration).FullName, typeof(TTarget).FullName); _disableIntegration = true; diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj b/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj index b63d15a15..cbe3fc9e8 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj +++ b/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj @@ -3,8 +3,16 @@ net462;netstandard2.0;netstandard2.1 false + $(DefineConstants);PROFILER_MANAGED + + + + + + + diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Logger.cs b/src/profiler/Elastic.Apm.Profiler.Managed/Logger.cs deleted file mode 100644 index a7f16666d..000000000 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Logger.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; - -namespace Elastic.Apm.Profiler.Managed -{ - // match the log levels of the profiler logger - internal enum LogLevel - { - Trace = 0, - Debug = 1, - Info = 2, - Warn = 3, - Error = 4, - Off = 5, - } - - internal static class Logger - { - static Logger() - { - Level = GetLogLevel(LogLevel.Warn); - var logDirectory = GetLogDirectory(); - LogFile = GetLogFile(logDirectory); - Levels = new Dictionary - { - [LogLevel.Off] = "OFF ", - [LogLevel.Error] = "ERROR", - [LogLevel.Warn] = "WARN ", - [LogLevel.Info] = "INFO ", - [LogLevel.Debug] = "DEBUG", - [LogLevel.Trace] = "TRACE", - }; - } - - private static readonly LogLevel Level; - private static readonly string LogFile; - private static readonly Dictionary Levels; - - public static void Log(LogLevel level, Exception exception, string message, params object[] args) - { - if (Level > level) - return; - - Log(level, $"{message}{Environment.NewLine}{exception}", args); - } - - public static void Debug(string message, params object[] args) => Log(LogLevel.Debug, message, args); - - public static void Error(Exception exception, string message, params object[] args) => Log(LogLevel.Error, exception, message, args); - - public static void Error(string message, params object[] args) => Log(LogLevel.Error, message, args); - - public static void Log(LogLevel level, string message, params object[] args) - { - if (Level > level) - return; - - try - { - if (LogFile != null) - { - try - { - using (var stream = File.Open(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read)) - using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) - { - writer.Write($"[{DateTimeOffset.Now:O}] [{Levels[level]}] "); - writer.WriteLine(message, args); - writer.Flush(); - stream.Flush(true); - } - - return; - } - catch - { - // ignore - } - } - - Console.Error.WriteLine($"[{DateTimeOffset.Now:O}] [{Levels[level]}] {message}", args); - } - catch - { - // ignore - } - } - - private static string GetLogFile(string logDirectory) - { - if (logDirectory is null) - return null; - - var process = Process.GetCurrentProcess(); - return Path.Combine(logDirectory, $"Elastic.Apm.Profiler.Managed_{process.ProcessName}_{process.Id}.log"); - } - - private static LogLevel GetLogLevel(LogLevel defaultLevel) - { - var level = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG"); - if (string.IsNullOrEmpty(level)) - return defaultLevel; - - return Enum.TryParse(level, true, out var parsedLevel) - ? parsedLevel - : defaultLevel; - } - - private static string GetLogDirectory() - { - try - { - var logDirectory = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_DIR"); - if (string.IsNullOrEmpty(logDirectory)) - { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - var programData = Environment.GetEnvironmentVariable("PROGRAMDATA"); - logDirectory = !string.IsNullOrEmpty(programData) - ? Path.Combine(programData, "elastic", "apm-agent-dotnet", "logs") - : "."; - } - else - logDirectory = "/var/log/elastic/apm-agent-dotnet"; - } - - Directory.CreateDirectory(logDirectory); - return logDirectory; - } - catch - { - return null; - } - } - } -} diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs index 542157c46..13e43b309 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs @@ -237,7 +237,7 @@ private TDelegate EmitDelegate() } } else - Logger.Log(LogLevel.Warn, "Unable to resolve module version id {0}. Using method builder fallback.", _moduleVersionId); + Logger.Warn("Unable to resolve module version id {0}. Using method builder fallback.", _moduleVersionId); MethodInfo methodInfo = null; @@ -384,26 +384,25 @@ private MethodInfo VerifyMethodFromToken(MethodInfo methodInfo) if (!string.Equals(_methodName, methodInfo.Name)) { - Logger.Log(LogLevel.Warn, $"Method name mismatch: {detailMessage}"); + Logger.Warn($"Method name mismatch: {detailMessage}"); return null; } if (!GenericsAreViable(methodInfo)) { - Logger.Log(LogLevel.Warn, $"Generics not viable: {detailMessage}"); + Logger.Warn($"Generics not viable: {detailMessage}"); return null; } if (!ParametersAreViable(methodInfo)) { - Logger.Log(LogLevel.Warn, $"Parameters not viable: {detailMessage}"); + Logger.Warn($"Parameters not viable: {detailMessage}"); return null; } if (!methodInfo.IsStatic && !methodInfo.ReflectedType.IsAssignableFrom(_concreteType)) { - Logger.Log(LogLevel.Warn, - $"{_concreteType} cannot be assigned to the type containing the MethodInfo representing the instance method: {detailMessage}"); + Logger.Warn($"{_concreteType} cannot be assigned to the type containing the MethodInfo representing the instance method: {detailMessage}"); return null; } @@ -453,7 +452,7 @@ private MethodInfo TryFindMethod() { var logDetail = $"mdToken {_mdToken} on {_concreteTypeName}.{_methodName} in {_resolutionModule?.FullyQualifiedName ?? "NULL"}, {_resolutionModule?.ModuleVersionId ?? _moduleVersionId}"; - Logger.Log(LogLevel.Warn, $"Using fallback method matching ({logDetail})"); + Logger.Warn($"Using fallback method matching ({logDetail})"); var methods = _concreteType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); @@ -495,7 +494,7 @@ private MethodInfo TryFindMethod() if (methods.Length == 1) { - Logger.Log(LogLevel.Info, $"Resolved by name and namespaceName filters ({logDetail})"); + Logger.Warn($"Resolved by name and namespaceName filters ({logDetail})"); return methods[0]; } @@ -506,7 +505,7 @@ private MethodInfo TryFindMethod() if (methods.Length == 1) { - Logger.Log(LogLevel.Info, $"Resolved by viable parameters ({logDetail})"); + Logger.Warn($"Resolved by viable parameters ({logDetail})"); return methods[0]; } @@ -517,7 +516,7 @@ private MethodInfo TryFindMethod() if (methods.Length == 1) { - Logger.Log(LogLevel.Info, $"Resolved by viable generics ({logDetail})"); + Logger.Warn($"Resolved by viable generics ({logDetail})"); return methods[0]; } diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs index cb206ba12..32f86659e 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs @@ -55,8 +55,7 @@ public static Module Get(Guid moduleVersionId) { if (_shortCircuitLogicHasLogged) return null; - Logger.Log(LogLevel.Warn, - "Elastic APM is unable to continue attempting module lookups for this AppDomain. Falling back to legacy method lookups."); + Logger.Warn("Elastic APM is unable to continue attempting module lookups for this AppDomain. Falling back to legacy method lookups."); _shortCircuitLogicHasLogged = true; } diff --git a/src/profiler/elastic_apm_profiler/src/profiler/env.rs b/src/profiler/elastic_apm_profiler/src/profiler/env.rs index bb8aea6cd..5538a4db8 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/env.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/env.rs @@ -23,28 +23,33 @@ use log4rs::{ use once_cell::sync::Lazy; use std::time::SystemTime; use std::{collections::HashSet, fs::File, io::BufReader, path::PathBuf, str::FromStr}; +use serde::__private; const APP_POOL_ID_ENV_VAR: &str = "APP_POOL_ID"; const DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR: &str = "DOTNET_CLI_TELEMETRY_PROFILE"; const COMPLUS_LOADEROPTIMIZATION: &str = "COMPLUS_LOADEROPTIMIZATION"; -const ELASTIC_APM_PROFILER_CALLTARGET_ENABLED_ENV_VAR: &str = - "ELASTIC_APM_PROFILER_CALLTARGET_ENABLED"; -const ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS_ENV_VAR: &str = - "ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS"; +const ELASTIC_APM_PROFILER_CALLTARGET_ENABLED_ENV_VAR: &str = "ELASTIC_APM_PROFILER_CALLTARGET_ENABLED"; +const ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS"; const ELASTIC_APM_PROFILER_ENABLE_INLINING_ENV_VAR: &str = "ELASTIC_APM_PROFILER_ENABLE_INLINING"; -const ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS_ENV_VAR: &str = - "ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS"; -const ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR: &str = - "ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES"; -const ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR: &str = - "ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES"; +const ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS"; +const ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR: &str = "ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES"; +const ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR: &str = "ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES"; const ELASTIC_APM_PROFILER_HOME_ENV_VAR: &str = "ELASTIC_APM_PROFILER_HOME"; const ELASTIC_APM_PROFILER_INTEGRATIONS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_INTEGRATIONS"; +const ELASTIC_APM_PROFILER_LOG_IL_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_IL"; + +const ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_TARGETS"; +const ELASTIC_OTEL_LOG_TARGETS_ENV_VAR: &str = "ELASTIC_OTEL_LOG_TARGETS"; + const ELASTIC_APM_PROFILER_LOG_DIR_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_DIR"; const ELASTIC_APM_PROFILER_LOG_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG"; -const ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_TARGETS"; -const ELASTIC_APM_PROFILER_LOG_IL_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_IL"; + +const OTEL_LOG_LEVEL_ENV_VAR: &str = "OTEL_LOG_LEVEL"; +const OTEL_DOTNET_AUTO_LOG_DIRECTORY_ENV_VAR: &str = "OTEL_DOTNET_AUTO_LOG_DIRECTORY"; + +const ELASTIC_APM_LOG_LEVEL_ENV_VAR: &str = "ELASTIC_APM_LOG_LEVEL"; +const ELASTIC_APM_LOG_DIRECTORY_ENV_VAR: &str = "ELASTIC_APM_LOG_DIRECTORY"; const ELASTIC_APM_SERVICE_NAME_ENV_VAR: &str = "ELASTIC_APM_SERVICE_NAME"; @@ -101,6 +106,7 @@ pub fn get_env_vars() -> String { let key = k.to_uppercase(); if key.starts_with("ELASTIC_") || key.starts_with("CORECLR_") + || key.starts_with("OTEL_") || key.starts_with("COR_") || key == APP_POOL_ID_ENV_VAR || key == DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR @@ -173,16 +179,24 @@ pub fn enable_inlining(default: bool) -> bool { read_bool_env_var(ELASTIC_APM_PROFILER_ENABLE_INLINING_ENV_VAR, default) } +fn to_target(value: String) -> HashSet { + value + .split(';') + .into_iter() + .filter_map(|s| match s.to_lowercase().as_str() { + out if out == "file" || out == "stdout" => Some(out.into()), + _ => None, + }) + .collect() +} + fn read_log_targets_from_env_var() -> HashSet { - let mut set = match std::env::var(ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR) { - Ok(value) => value - .split(';') - .into_iter() - .filter_map(|s| match s.to_lowercase().as_str() { - out if out == "file" || out == "stdout" => Some(out.into()), - _ => None, - }) - .collect(), + let mut set = match ( + std::env::var(ELASTIC_OTEL_LOG_TARGETS_ENV_VAR), + std::env::var(ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR) + ) { + (Ok(value), _) => to_target(value), + (_, Ok(value)) => to_target(value), _ => HashSet::with_capacity(1), }; @@ -193,8 +207,14 @@ fn read_log_targets_from_env_var() -> HashSet { } pub fn read_log_level_from_env_var(default: LevelFilter) -> LevelFilter { - match std::env::var(ELASTIC_APM_PROFILER_LOG_ENV_VAR) { - Ok(value) => LevelFilter::from_str(value.as_str()).unwrap_or(default), + match ( + std::env::var(OTEL_LOG_LEVEL_ENV_VAR), + std::env::var(ELASTIC_APM_PROFILER_LOG_ENV_VAR), + std::env::var(ELASTIC_APM_LOG_DIRECTORY_ENV_VAR) + ) { + (Ok(value), _, _) => LevelFilter::from_str(value.as_str()).unwrap_or(default), + (_, Ok(value), _) => LevelFilter::from_str(value.as_str()).unwrap_or(default), + (_, _, Ok(value)) => LevelFilter::from_str(value.as_str()).unwrap_or(default), _ => default, } } @@ -291,9 +311,15 @@ fn get_home_log_dir() -> PathBuf { } fn get_log_dir() -> PathBuf { - match std::env::var(ELASTIC_APM_PROFILER_LOG_DIR_ENV_VAR) { - Ok(path) => PathBuf::from(path), - Err(_) => get_default_log_dir(), + match ( + std::env::var(OTEL_DOTNET_AUTO_LOG_DIRECTORY_ENV_VAR), + std::env::var(ELASTIC_APM_PROFILER_LOG_DIR_ENV_VAR), + std::env::var(ELASTIC_APM_LOG_DIRECTORY_ENV_VAR), + ) { + (Ok(path), _, _) => PathBuf::from(path), + (_, Ok(path), _) => PathBuf::from(path), + (_, _, Ok(path)) => PathBuf::from(path), + _ => get_default_log_dir(), } } diff --git a/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj b/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj index 19d9afedc..60984f437 100644 --- a/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj +++ b/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj @@ -16,10 +16,4 @@ - - - StartupHookLogger.cs - - - diff --git a/src/startuphook/Elastic.Apm.StartupHook.Loader/Loader.cs b/src/startuphook/Elastic.Apm.StartupHook.Loader/Loader.cs index 2a81154b6..0e3d5677d 100644 --- a/src/startuphook/Elastic.Apm.StartupHook.Loader/Loader.cs +++ b/src/startuphook/Elastic.Apm.StartupHook.Loader/Loader.cs @@ -13,7 +13,8 @@ using Elastic.Apm.Extensions.Hosting; using Elastic.Apm.GrpcClient; using Elastic.Apm.Instrumentations.SqlClient; -using ElasticApmStartupHook; +using Elastic.Apm.Logging; +using IApmLogger = Elastic.Apm.Logging.IApmLogger; namespace Elastic.Apm.StartupHook.Loader { @@ -22,26 +23,15 @@ namespace Elastic.Apm.StartupHook.Loader /// internal class Loader { - /// - /// The directory in which the executing assembly is located - /// - private static string AssemblyDirectory - { - get - { - var location = Assembly.GetExecutingAssembly().Location; - return Path.GetDirectoryName(location); - } - } - /// /// Initializes and starts the agent /// public static void Initialize() { - Agent.Setup(new AgentComponents()); + var agentComponents = new AgentComponents(); + Agent.Setup(agentComponents); - var logger = StartupHookLogger.Create(); + var logger = agentComponents.Logger; LoadDiagnosticSubscriber(new HttpDiagnosticsSubscriber(), logger); LoadDiagnosticSubscriber(new AspNetCoreDiagnosticSubscriber(), logger); LoadDiagnosticSubscriber(new EfCoreDiagnosticsSubscriber(), logger); @@ -51,7 +41,7 @@ public static void Initialize() HostBuilderExtensions.UpdateServiceInformation(Agent.Instance.Service); - static void LoadDiagnosticSubscriber(IDiagnosticsSubscriber diagnosticsSubscriber, StartupHookLogger logger) + static void LoadDiagnosticSubscriber(IDiagnosticsSubscriber diagnosticsSubscriber, IApmLogger logger) { try { @@ -59,8 +49,7 @@ static void LoadDiagnosticSubscriber(IDiagnosticsSubscriber diagnosticsSubscribe } catch (Exception e) { - logger.WriteLine($"Failed subscribing to {diagnosticsSubscriber.GetType().Name}, " + - $"Exception type: {e.GetType().Name}, message: {e.Message}"); + logger.Error()?.LogException(e, $"Failed subscribing to {diagnosticsSubscriber.GetType().Name}"); } } } diff --git a/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj b/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj index 5c79c06d5..f75da942b 100644 --- a/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj +++ b/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj @@ -4,7 +4,12 @@ ElasticApmAgentStartupHook false ElasticApmStartupHook + $(DefineConstants);STARTUP_HOOKS + + + + diff --git a/src/startuphook/ElasticApmAgentStartupHook/StartupHookLogger.cs b/src/startuphook/ElasticApmAgentStartupHook/StartupHookLogger.cs index 7bbf532ef..69ea6fd0a 100644 --- a/src/startuphook/ElasticApmAgentStartupHook/StartupHookLogger.cs +++ b/src/startuphook/ElasticApmAgentStartupHook/StartupHookLogger.cs @@ -35,13 +35,10 @@ private StartupHookLogger(string logPath, bool enabled) /// public static StartupHookLogger Create() { - var startupHookEnvVar = Environment.GetEnvironmentVariable("DOTNET_STARTUP_HOOKS"); + var config = GlobalLogConfiguration.FromEnvironment(Environment.GetEnvironmentVariables()); + var path = config.CreateLogFileName("startup_hook"); - var startupHookDirectory = Path.GetDirectoryName(startupHookEnvVar); - var startupHookLoggingEnvVar = Environment.GetEnvironmentVariable("ELASTIC_APM_STARTUP_HOOKS_LOGGING"); - - return new StartupHookLogger(Path.Combine(startupHookDirectory, "ElasticApmAgentStartupHook.log"), - !string.IsNullOrEmpty(startupHookLoggingEnvVar)); + return new StartupHookLogger(path, config.IsActive); } public void WriteLine(string message) diff --git a/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs new file mode 100644 index 000000000..75c0db47d --- /dev/null +++ b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs @@ -0,0 +1,82 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections; +using Elastic.Apm.Logging; +using FluentAssertions; +using Xunit; +using static Elastic.Apm.Logging.LogEnvironmentVariables; + +namespace Elastic.Apm.Tests.Config; + +public class GlobalLogConfigurationPrecedenceTests +{ + [Fact] + public void CheckLogLevelPrecedence() + { + var config = CreateConfig([ + (OTEL_LOG_LEVEL, "trace"), + (ELASTIC_APM_PROFILER_LOG, "info"), + (ELASTIC_APM_LOG_LEVEL, "error"), + ]); + config.LogLevel.Should().Be(LogLevel.Trace); + + config = CreateConfig([ + (ELASTIC_APM_PROFILER_LOG, "info"), + (ELASTIC_APM_LOG_LEVEL, "error"), + ]); + config.LogLevel.Should().Be(LogLevel.Information); + + config = CreateConfig([ + (ELASTIC_APM_LOG_LEVEL, "error"), + ]); + config.LogLevel.Should().Be(LogLevel.Error); + } + + [Fact] + public void CheckLogDirPrecedence() + { + var config = CreateConfig([ + (OTEL_DOTNET_AUTO_LOG_DIRECTORY, nameof(OTEL_DOTNET_AUTO_LOG_DIRECTORY)), + (ELASTIC_APM_PROFILER_LOG_DIR, nameof(ELASTIC_APM_PROFILER_LOG_DIR)), + (ELASTIC_APM_LOG_DIRECTORY, nameof(ELASTIC_APM_LOG_DIRECTORY)), + ]); + config.LogFileDirectory.Should().Be(nameof(OTEL_DOTNET_AUTO_LOG_DIRECTORY)); + + config = CreateConfig([ + (ELASTIC_APM_PROFILER_LOG_DIR, nameof(ELASTIC_APM_PROFILER_LOG_DIR)), + (ELASTIC_APM_LOG_DIRECTORY, nameof(ELASTIC_APM_LOG_DIRECTORY)), + ]); + config.LogFileDirectory.Should().Be(nameof(ELASTIC_APM_PROFILER_LOG_DIR)); + + config = CreateConfig([ + (ELASTIC_APM_LOG_DIRECTORY, nameof(ELASTIC_APM_LOG_DIRECTORY)), + ]); + config.LogFileDirectory.Should().Be(nameof(ELASTIC_APM_LOG_DIRECTORY)); + } + + [Fact] + public void CheckLogTargetsPrecedence() + { + var config = CreateConfig([ + (ELASTIC_OTEL_LOG_TARGETS, "stdout"), + (ELASTIC_APM_PROFILER_LOG_TARGETS, "stdout;file"), + ]); + config.LogTargets.Should().Be(GlobalLogTarget.StdOut); + + config = CreateConfig([ + (ELASTIC_APM_PROFILER_LOG_TARGETS, "stdout;file"), + ]); + config.LogTargets.Should().Be(GlobalLogTarget.StdOut | GlobalLogTarget.File); + } + + private static GlobalLogConfiguration CreateConfig(params (string key, string v)[] values) + { + var environment = new Hashtable(); + foreach (var kv in values) + environment.Add(kv.key, kv.v); + var config = GlobalLogConfiguration.FromEnvironment(environment); + return config; + } +} diff --git a/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationTests.cs b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationTests.cs new file mode 100644 index 000000000..63cf310f9 --- /dev/null +++ b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationTests.cs @@ -0,0 +1,176 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections; +using Elastic.Apm.Config; +using Elastic.Apm.Logging; +using FluentAssertions; +using Xunit; +using static Elastic.Apm.Logging.LogEnvironmentVariables; + +namespace Elastic.Apm.Tests.Config +{ + public class GlobalLogConfigurationTests + { + [Fact] + public void Check_Defaults() + { + var config = GlobalLogConfiguration.FromEnvironment(new Hashtable()); + config.IsActive.Should().BeFalse(); + config.LogLevel.Should().Be(LogLevel.Warning); + config.AgentLogFilePath.Should().StartWith(EnvironmentLoggingConfiguration.GetDefaultLogDirectory()); + config.AgentLogFilePath.Should().EndWith(".agent.log"); + //because is active is false log targets defaults to none; + config.LogTargets.Should().Be(GlobalLogTarget.None); + } + + + // + [Theory] + [InlineData(OTEL_LOG_LEVEL, "Debug")] + [InlineData(ELASTIC_APM_LOG_LEVEL, "Debug")] + [InlineData(ELASTIC_APM_PROFILER_LOG, "Info")] + [InlineData(OTEL_DOTNET_AUTO_LOG_DIRECTORY, "1")] + [InlineData(ELASTIC_APM_LOG_DIRECTORY, "1")] + [InlineData(ELASTIC_APM_PROFILER_LOG_DIR, "1")] + [InlineData(ELASTIC_APM_STARTUP_HOOKS_LOGGING, "1")] + //only if explicitly specified to 'none' should we not default to file logging. + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "file")] + [InlineData(ELASTIC_APM_PROFILER_LOG_TARGETS, "file")] + public void CheckActivation(string environmentVariable, string value) + { + var config = GlobalLogConfiguration.FromEnvironment(new Hashtable { { environmentVariable, value } }); + config.IsActive.Should().BeTrue(); + config.LogTargets.Should().Be(GlobalLogTarget.File); + } + + // + [Theory] + [InlineData(OTEL_LOG_LEVEL, "none")] + [InlineData(OTEL_LOG_LEVEL, "Info")] + [InlineData(ELASTIC_APM_LOG_LEVEL, "Info")] + [InlineData(ELASTIC_APM_PROFILER_LOG, "None")] + //only if explicitly specified to 'none' should we not default to file logging. + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "none")] + [InlineData(ELASTIC_APM_PROFILER_LOG_TARGETS, "none")] + public void CheckDeactivation(string environmentVariable, string value) + { + var config = GlobalLogConfiguration.FromEnvironment(new Hashtable + { + { OTEL_DOTNET_AUTO_LOG_DIRECTORY, "" }, + { environmentVariable, value } + }); + config.IsActive.Should().BeFalse(); + config.LogTargets.Should().Be(GlobalLogTarget.None); + } + + [Theory] + //only specifying apm_log_level not sufficient, needs explicit directory configuration + [InlineData(ELASTIC_APM_LOG_LEVEL, "Warning")] + //setting targets to none will result in no global trace logging + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "None")] + [InlineData(ELASTIC_APM_PROFILER_LOG_TARGETS, "None")] + //setting file log level to none will result in no global trace logging + [InlineData(OTEL_LOG_LEVEL, "None")] + //setting profiler log level to none will result in no global trace logging + [InlineData(ELASTIC_APM_PROFILER_LOG, "None")] + public void CheckNonActivation(string environmentVariable, string value) + { + var config = GlobalLogConfiguration.FromEnvironment(new Hashtable { { environmentVariable, value } }); + config.IsActive.Should().BeFalse(); + } + + [Theory] + [InlineData("trace", LogLevel.Trace)] + [InlineData("Trace", LogLevel.Trace)] + [InlineData("TraCe", LogLevel.Trace)] + [InlineData("debug", LogLevel.Debug)] + [InlineData("info", LogLevel.Information)] + [InlineData("warn", LogLevel.Warning)] + [InlineData("error", LogLevel.Error)] + [InlineData("none", LogLevel.None)] + public void Check_LogLevelValues_AreMappedCorrectly(string envVarValue, LogLevel logLevel) + { + Check(ELASTIC_APM_PROFILER_LOG, envVarValue, logLevel); + Check(ELASTIC_APM_LOG_LEVEL, envVarValue, logLevel); + Check(OTEL_LOG_LEVEL, envVarValue, logLevel); + return; + + static void Check(string key, string envVarValue, LogLevel level) + { + var config = CreateConfig(key, envVarValue); + config.LogLevel.Should().Be(level, "{0}", key); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("foo")] + [InlineData("tracing")] + public void Check_InvalidLogLevelValues_AreMappedToDefaultWarn(string envVarValue) + { + Check(ELASTIC_APM_PROFILER_LOG, envVarValue); + Check(ELASTIC_APM_LOG_LEVEL, envVarValue); + Check(OTEL_LOG_LEVEL, envVarValue); + return; + + static void Check(string key, string envVarValue) + { + var config = CreateConfig(key, envVarValue); + config.LogLevel.Should().Be(LogLevel.Warning, "{0}", key); + } + } + + [Fact] + public void Check_LogDir_IsEvaluatedCorrectly() + { + Check(ELASTIC_APM_PROFILER_LOG_DIR, "/foo/bar"); + Check(ELASTIC_APM_LOG_DIRECTORY, "/foo/bar"); + Check(OTEL_DOTNET_AUTO_LOG_DIRECTORY, "/foo/bar"); + return; + + static void Check(string key, string envVarValue) + { + var config = CreateConfig(key, envVarValue); + config.AgentLogFilePath.Should().StartWith("/foo/bar", "{0}", key); + config.AgentLogFilePath.Should().EndWith(".agent.log", "{0}", key); + } + } + + [Theory] + [InlineData(null, GlobalLogTarget.None)] + [InlineData("", GlobalLogTarget.None)] + [InlineData("foo", GlobalLogTarget.None)] + [InlineData("foo,bar", GlobalLogTarget.None)] + [InlineData("foo;bar", GlobalLogTarget.None)] + [InlineData("file;foo;bar", GlobalLogTarget.File)] + [InlineData("file", GlobalLogTarget.File)] + [InlineData("stdout", GlobalLogTarget.StdOut)] + [InlineData("StdOut", GlobalLogTarget.StdOut)] + [InlineData("file;stdout", GlobalLogTarget.File | GlobalLogTarget.StdOut)] + [InlineData("FILE;StdOut", GlobalLogTarget.File | GlobalLogTarget.StdOut)] + [InlineData("file;stdout;file", GlobalLogTarget.File | GlobalLogTarget.StdOut)] + [InlineData("FILE;StdOut;stdout", GlobalLogTarget.File | GlobalLogTarget.StdOut)] + internal void Check_LogTargets_AreEvaluatedCorrectly(string envVarValue, GlobalLogTarget? targets) + { + Check(ELASTIC_APM_PROFILER_LOG_TARGETS, envVarValue, targets); + Check(ELASTIC_OTEL_LOG_TARGETS, envVarValue, targets); + return; + + static void Check(string key, string envVarValue, GlobalLogTarget? targets) + { + var config = CreateConfig(key, envVarValue); + config.LogTargets.Should().Be(targets, "{0}", key); + } + } + + private static GlobalLogConfiguration CreateConfig(string key, string envVarValue) + { + var environment = new Hashtable { { key, envVarValue } }; + var config = GlobalLogConfiguration.FromEnvironment(environment); + return config; + } + } +} diff --git a/test/Elastic.Apm.Tests/Config/ProfilerLogConfigTests.cs b/test/Elastic.Apm.Tests/Config/ProfilerLogConfigTests.cs deleted file mode 100644 index 42420941a..000000000 --- a/test/Elastic.Apm.Tests/Config/ProfilerLogConfigTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Collections; -using Elastic.Apm.Config; -using Elastic.Apm.Logging; -using FluentAssertions; -using Xunit; - -namespace Elastic.Apm.Tests.Config -{ - public class ProfilerLogConfigTests - { - [Fact] - public void Check_Defaults() - { - var config = ProfilerLogConfig.Check(new Hashtable()); - config.IsActive.Should().BeFalse(); - config.LogLevel.Should().Be(LogLevel.Warning); - config.LogFilePath.Should().StartWith(ProfilerLogConfig.GetDefaultProfilerLogDirectory()); - config.LogFilePath.Should().EndWith(".agent.log"); - config.LogTargets.Should().Be(ProfilerLogTarget.File); - } - - [Theory] - [InlineData("trace", LogLevel.Trace)] - [InlineData("Trace", LogLevel.Trace)] - [InlineData("TraCe", LogLevel.Trace)] - [InlineData("debug", LogLevel.Debug)] - [InlineData("info", LogLevel.Information)] - [InlineData("warn", LogLevel.Warning)] - [InlineData("error", LogLevel.Error)] - [InlineData("none", LogLevel.None)] - public void Check_LogLevelValues_AreMappedCorrectly(string envVarValue, LogLevel logLevel) - { - var environment = new Hashtable { { "ELASTIC_APM_PROFILER_LOG", envVarValue } }; - var config = ProfilerLogConfig.Check(environment); - config.IsActive.Should().BeTrue(); - config.LogLevel.Should().Be(logLevel); - } - - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("foo", true)] - [InlineData("tracing", true)] - public void Check_InvalidLogLevelValues_AreMappedToDefaultWarn(string envVarValue, bool isActive) - { - var environment = new Hashtable { { "ELASTIC_APM_PROFILER_LOG", envVarValue } }; - var config = ProfilerLogConfig.Check(environment); - config.LogLevel.Should().Be(LogLevel.Warning); - config.IsActive.Should().Be(isActive); - } - - [Fact] - public void Check_LogDir_IsEvaluatedCorrectly() - { - var environment = new Hashtable { { "ELASTIC_APM_PROFILER_LOG_DIR", "/foo/bar" } }; - var config = ProfilerLogConfig.Check(environment); - config.LogFilePath.Should().StartWith("/foo/bar"); - config.LogFilePath.Should().EndWith(".agent.log"); - } - - [Theory] - [InlineData(null, ProfilerLogTarget.File)] - [InlineData("", ProfilerLogTarget.File)] - [InlineData("foo", ProfilerLogTarget.File)] - [InlineData("foo,bar", ProfilerLogTarget.File)] - [InlineData("foo;bar", ProfilerLogTarget.File)] - [InlineData("file;foo;bar", ProfilerLogTarget.File)] - [InlineData("file", ProfilerLogTarget.File)] - [InlineData("stdout", ProfilerLogTarget.StdOut)] - [InlineData("StdOut", ProfilerLogTarget.StdOut)] - [InlineData("file;stdout", ProfilerLogTarget.File | ProfilerLogTarget.StdOut)] - [InlineData("FILE;StdOut", ProfilerLogTarget.File | ProfilerLogTarget.StdOut)] - [InlineData("file;stdout;file", ProfilerLogTarget.File | ProfilerLogTarget.StdOut)] - [InlineData("FILE;StdOut;stdout", ProfilerLogTarget.File | ProfilerLogTarget.StdOut)] - internal void Check_LogTargets_AreEvaluatedCorrectly(string envVarValue, ProfilerLogTarget targets) - { - var environment = new Hashtable { { "ELASTIC_APM_PROFILER_LOG_TARGETS", envVarValue } }; - var config = ProfilerLogConfig.Check(environment); - config.LogTargets.Should().Be(targets); - } - } -} diff --git a/test/Elastic.Apm.Tests/LoggerTests.cs b/test/Elastic.Apm.Tests/LoggerTests.cs index 54c05c334..5952a3713 100644 --- a/test/Elastic.Apm.Tests/LoggerTests.cs +++ b/test/Elastic.Apm.Tests/LoggerTests.cs @@ -26,22 +26,22 @@ public void CheckForProfilerLogger_ReturnsExpectedLoggers() { var fallbackLogger = new NoopLogger(); - var logger = AgentComponents.CheckForProfilerLogger(fallbackLogger, LogLevel.Trace, new Hashtable()); + var logger = AgentComponents.GetGlobalLogger(fallbackLogger, LogLevel.Trace, new Hashtable()); logger.Should().NotBeNull(); logger.Should().Be(fallbackLogger); logger.IsEnabled(LogLevel.Trace).Should().BeFalse(); - logger = AgentComponents.CheckForProfilerLogger(fallbackLogger, LogLevel.Trace, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "trace" } }); + logger = AgentComponents.GetGlobalLogger(fallbackLogger, LogLevel.Trace, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "trace" } }); logger.Should().NotBeNull(); logger.Should().NotBe(fallbackLogger); logger.IsEnabled(LogLevel.Trace).Should().BeTrue(); - logger = AgentComponents.CheckForProfilerLogger(fallbackLogger, LogLevel.Error, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "trace" } }); + logger = AgentComponents.GetGlobalLogger(fallbackLogger, LogLevel.Error, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "trace" } }); logger.Should().NotBeNull(); logger.Should().NotBe(fallbackLogger); logger.IsEnabled(LogLevel.Trace).Should().BeTrue(); - logger = AgentComponents.CheckForProfilerLogger(fallbackLogger, LogLevel.Error, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "warn" } }); + logger = AgentComponents.GetGlobalLogger(fallbackLogger, LogLevel.Error, new Hashtable { { "ELASTIC_APM_PROFILER_LOG", "warn" } }); logger.Should().NotBeNull(); logger.Should().NotBe(fallbackLogger); logger.IsEnabled(LogLevel.Trace).Should().BeFalse(); diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs index 8c0480aea..1a23b5c01 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs @@ -20,9 +20,9 @@ public void UseElasticApmShouldUseAspNetLoggerWhenLoggingIsConfigured() var services = new ServiceCollection() .AddLogging(); - var logger = NetCoreLogger.GetApmLogger(services.BuildServiceProvider()); + var logger = ApmExtensionsLogger.GetApmLogger(services.BuildServiceProvider()); - Assert.IsType(logger); + Assert.IsType(logger); } [Fact] @@ -30,7 +30,7 @@ public void UseElasticApmShouldUseConsoleLoggerInstanceWhenLoggingIsNotConfigure { var services = new ServiceCollection(); - var logger = NetCoreLogger.GetApmLogger(services.BuildServiceProvider()); + var logger = ApmExtensionsLogger.GetApmLogger(services.BuildServiceProvider()); Assert.IsType(logger); Assert.Same(ConsoleLogger.Instance, logger); diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs index 23edf20e0..10f38113d 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs @@ -24,6 +24,7 @@ using SampleAspNetCoreApp; using Xunit; using Xunit.Abstractions; +using IApmLogger = Elastic.Apm.Logging.IApmLogger; namespace Elastic.Apm.AspNetCore.Tests { diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreLoggerTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreLoggerTests.cs index a6e4476b1..98e1844ba 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreLoggerTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreLoggerTests.cs @@ -11,13 +11,13 @@ namespace Elastic.Apm.AspNetCore.Tests { /// - /// Tests the type. + /// Tests the type. /// public class AspNetCoreLoggerTests { [Fact] public void AspNetCoreLoggerShouldThrowExceptionWhenLoggerFactoryIsNull() - => Assert.Throws(() => new NetCoreLogger(null)); + => Assert.Throws(() => new ApmExtensionsLogger(null)); [Fact] public void AspNetCoreLoggerShouldGetLoggerFromFactoryWithProperCategoryName() @@ -27,7 +27,7 @@ public void AspNetCoreLoggerShouldGetLoggerFromFactoryWithProperCategoryName() .Returns(() => Mock.Of()); // ReSharper disable UnusedVariable - var logger = new NetCoreLogger(loggerFactoryMock.Object); + var logger = new ApmExtensionsLogger(loggerFactoryMock.Object); // ReSharper restore UnusedVariable loggerFactoryMock.Verify(x => x.CreateLogger(It.Is(s => s.Equals("Elastic.Apm"))), Times.Once); diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/TransactionIgnoreUrlsTest.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/TransactionIgnoreUrlsTest.cs index 369380440..7cd8c4aab 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/TransactionIgnoreUrlsTest.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/TransactionIgnoreUrlsTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using SampleAspNetCoreApp; using Xunit; +using IApmLogger = Elastic.Apm.Logging.IApmLogger; namespace Elastic.Apm.AspNetCore.Tests { diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/BasicTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/BasicTests.cs index 1a956ecb9..e60c24d16 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/BasicTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/BasicTests.cs @@ -12,6 +12,7 @@ using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; using Xunit.Abstractions; +using static Elastic.Apm.Logging.LogEnvironmentVariables; namespace Elastic.Apm.Profiler.Managed.Tests; @@ -38,7 +39,7 @@ public async Task AgentVersionTest() { ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_DISABLE_METRICS"] = "*", - ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + [ELASTIC_OTEL_LOG_TARGETS] = "file;stdout", }; profiledApplication.Start( diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs index f58479399..0564df106 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs @@ -13,6 +13,7 @@ using FluentAssertions; using Xunit; using Xunit.Abstractions; +using static Elastic.Apm.Logging.LogEnvironmentVariables; namespace Elastic.Apm.Profiler.Managed.Tests; @@ -39,7 +40,7 @@ public async Task ShouldNotInstrumentExcludedIntegrations() ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS"] = "SqliteCommand;AdoNet", ["ELASTIC_APM_DISABLE_METRICS"] = "*", - ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout" + [ELASTIC_OTEL_LOG_TARGETS] = "file;stdout" }; profiledApplication.Start( @@ -95,7 +96,7 @@ public async Task ShouldNotInstrumentExcludedProcess(string targetFramework, str { ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_DISABLE_METRICS"] = "*", - ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + [ELASTIC_OTEL_LOG_TARGETS] = "file;stdout", ["ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES"] = excludeProcess }; @@ -142,7 +143,7 @@ public async Task ShouldNotInstrumentExcludedServiceName() { ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_DISABLE_METRICS"] = "*", - ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + [ELASTIC_OTEL_LOG_TARGETS] = "file;stdout", ["ELASTIC_APM_SERVICE_NAME"] = serviceName, ["ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES"] = serviceName }; diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs index 5e6fa0c81..ab9a523b6 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs @@ -13,6 +13,7 @@ using Elastic.Apm.Tests.Utilities; using ProcNet; using ProcNet.Std; +using static Elastic.Apm.Logging.LogEnvironmentVariables; namespace Elastic.Apm.Profiler.Managed.Tests { @@ -145,11 +146,11 @@ public void Start( environmentVariables["ELASTIC_APM_PROFILER_INTEGRATIONS"] = Path.Combine(SolutionPaths.Root, "src", "profiler", "Elastic.Apm.Profiler.Managed", "integrations.yml"); - environmentVariables["ELASTIC_APM_PROFILER_LOG"] = "trace"; + environmentVariables[OTEL_LOG_LEVEL] = "trace"; // log to relative logs directory for managed loader - environmentVariables["ELASTIC_APM_PROFILER_LOG_DIR"] = Path.Combine(SolutionPaths.Root, "logs"); + environmentVariables[OTEL_DOTNET_AUTO_LOG_DIRECTORY] = Path.Combine(SolutionPaths.Root, "logs"); - environmentVariables["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout"; + environmentVariables[ELASTIC_OTEL_LOG_TARGETS] = "file;stdout"; //environmentVariables["ELASTIC_APM_PROFILER_LOG_IL"] = "true"; // use the .exe for net462 diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/SatelliteAssemblyTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/SatelliteAssemblyTests.cs index ca3ba9cc9..ac9a5c01a 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/SatelliteAssemblyTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/SatelliteAssemblyTests.cs @@ -12,6 +12,7 @@ using FluentAssertions; using Xunit; using Xunit.Abstractions; +using static Elastic.Apm.Logging.LogEnvironmentVariables; namespace Elastic.Apm.Profiler.Managed.Tests; @@ -36,7 +37,7 @@ public async Task CorrectlyReadSatelliteAssemblyMetadata() { ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_DISABLE_METRICS"] = "*", - ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + [ELASTIC_OTEL_LOG_TARGETS] = "file;stdout", ["ELASTIC_APM_PROFILER_LOG"] = "debug", }; From 93ee4cc50076c2464bd48a647af471ae0750cbdd Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 23 Jul 2024 14:21:01 +0200 Subject: [PATCH 09/77] Remove service principal from test infra (#2391) --- .../Azure/AzureCredentials.cs | 59 ++----------------- 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs index 32036e9e1..76c0dbbb8 100644 --- a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs @@ -4,10 +4,7 @@ // See the LICENSE file in the project root for more information using System; -using System.Collections.Generic; -using System.IO; using System.Threading; -using Elastic.Apm.Libraries.Newtonsoft.Json; using ProcNet; namespace Elastic.Apm.Tests.Utilities.Azure @@ -15,64 +12,16 @@ namespace Elastic.Apm.Tests.Utilities.Azure /// /// Unauthenticated Azure credentials /// - public class Unauthenticated : AzureCredentials - { - } + public class Unauthenticated : AzureCredentials; /// /// Azure credentials authentication with a User account. /// - public class AzureUserAccount : AzureCredentials - { - } - - /// - /// Azure credentials authenticated with a Service Principal - /// - public class ServicePrincipal : AzureCredentials - { - [JsonConstructor] - private ServicePrincipal() { } - - [JsonProperty("clientId")] - public string ClientId { get; private set; } - - [JsonProperty("clientSecret")] - public string ClientSecret { get; private set; } - - [JsonProperty("tenantId")] - public string TenantId { get; private set; } - - [JsonProperty("subscriptionId")] - public string SubscriptionId { get; private set; } - - public ServicePrincipal(string clientId, string clientSecret, string tenantId, string subscriptionId) - { - ClientId = clientId; - ClientSecret = clientSecret; - TenantId = tenantId; - SubscriptionId = subscriptionId; - } - public override void AddToArguments(StartArguments startArguments) - { - startArguments.Environment ??= new Dictionary(); - startArguments.Environment[ARM_CLIENT_ID] = ClientId; - startArguments.Environment[ARM_CLIENT_SECRET] = ClientSecret; - startArguments.Environment[ARM_SUBSCRIPTION_ID] = SubscriptionId; - startArguments.Environment[ARM_TENANT_ID] = TenantId; - } - } + public class AzureUserAccount : AzureCredentials; public abstract class AzureCredentials { - // ReSharper disable InconsistentNaming - protected const string ARM_CLIENT_ID = nameof(ARM_CLIENT_ID); - protected const string ARM_CLIENT_SECRET = nameof(ARM_CLIENT_SECRET); - protected const string ARM_TENANT_ID = nameof(ARM_TENANT_ID); - protected const string ARM_SUBSCRIPTION_ID = nameof(ARM_SUBSCRIPTION_ID); - // ReSharper restore InconsistentNaming - - private static readonly Lazy _lazyCredentials = + private static readonly Lazy LazyCredentials = new(LoadCredentials, LazyThreadSafetyMode.ExecutionAndPublication); private static AzureCredentials LoadCredentials() => @@ -111,7 +60,7 @@ private static bool LoggedIntoAccountWithAzureCli() /// A set of Azure credentials obtained from environment variables or account authenticated with Azure CLI 2.0. /// If no credentials are found, an unauthenticated credential is returned. /// - public static AzureCredentials Instance => _lazyCredentials.Value; + public static AzureCredentials Instance => LazyCredentials.Value; public virtual void AddToArguments(StartArguments startArguments) { } } From 01708e3fbc09584d8d0a5b908dae63f8bd2bec44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:52:07 +0200 Subject: [PATCH 10/77] Bump the github-actions group across 1 directory with 4 updates (#2403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 4 updates in the / directory: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/login-action](https://github.com/docker/login-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `actions/attest-build-provenance` from 1.3.2 to 1.3.3
Release notes

Sourced from actions/attest-build-provenance's releases.

v1.3.3

What's Changed

Full Changelog: https://github.com/actions/attest-build-provenance/compare/v1.3.2...v1.3.3

Commits
  • 5e9cb68 bump actions/attest from 1.3.2 to 1.3.3 (#152)
  • 38faaec Bump the npm-development group with 5 updates (#149)
  • 833c4a9 Bump the npm-development group across 1 directory with 5 updates (#141)
  • See full diff in compare view

Updates `docker/setup-buildx-action` from 3.3.0 to 3.5.0
Release notes

Sourced from docker/setup-buildx-action's releases.

v3.5.0

Full Changelog: https://github.com/docker/setup-buildx-action/compare/v3.4.0...v3.5.0

v3.4.0

Full Changelog: https://github.com/docker/setup-buildx-action/compare/v3.3.0...v3.4.0

Commits
  • aa33708 Merge pull request #345 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • 2d99e34 chore: update generated content
  • 4dab436 build(deps): bump @​docker/actions-toolkit from 0.34.0 to 0.35.0
  • 49a04d6 Merge pull request #344 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • a6ade2e chore: update generated content
  • 2f2694b switch to Docker exec
  • 0a4bab6 build(deps): bump @​docker/actions-toolkit from 0.32.0 to 0.34.0
  • 2ad1852 Merge pull request #340 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • 560ac46 chore: update generated content
  • b3a3417 build(deps): bump @​docker/actions-toolkit from 0.31.0 to 0.32.0
  • Additional commits viewable in compare view

Updates `docker/login-action` from 3.2.0 to 3.3.0
Release notes

Sourced from docker/login-action's releases.

v3.3.0

Full Changelog: https://github.com/docker/login-action/compare/v3.2.0...v3.3.0

Commits
  • 9780b0c Merge pull request #741 from docker/dependabot/npm_and_yarn/proxy-agent-depen...
  • 2fa130c chore: update generated content
  • 5e87b2a build(deps): bump https-proxy-agent
  • e039495 Merge pull request #754 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • 9af18aa chore: update generated content
  • 668190a switch to Docker exec
  • be5150d build(deps): bump @​docker/actions-toolkit from 0.24.0 to 0.35.0
  • e80ebca Merge pull request #730 from docker/dependabot/npm_and_yarn/braces-3.0.3
  • 75ee3ea Merge pull request #733 from docker/dependabot/github_actions/docker/bake-act...
  • 793c19c build(deps): bump docker/bake-action from 4 to 5
  • Additional commits viewable in compare view

Updates `docker/build-push-action` from 6.1.0 to 6.5.0
Release notes

Sourced from docker/build-push-action's releases.

v6.5.0

Full Changelog: https://github.com/docker/build-push-action/compare/v6.4.1...v6.5.0

v6.4.1

Full Changelog: https://github.com/docker/build-push-action/compare/v6.4.0...v6.4.1

v6.4.0

Full Changelog: https://github.com/docker/build-push-action/compare/v6.3.0...v6.4.0

v6.3.0

Full Changelog: https://github.com/docker/build-push-action/compare/v6.2.0...v6.3.0

v6.2.0

Full Changelog: https://github.com/docker/build-push-action/compare/v6.1.0...v6.2.0

Commits
  • 5176d81 Merge pull request #1191 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • ec10ae8 chore: update generated content
  • 597e8fc chore(deps): Bump @​docker/actions-toolkit from 0.34.0 to 0.35.0
  • e050dfa Merge pull request #1186 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • d1fcdb6 chore: update generated content
  • a6067b9 chore(deps): Bump @​docker/actions-toolkit from 0.33.0 to 0.34.0
  • 1ca370b Merge pull request #1183 from crazy-max/revert-gha-cache-to
  • 2c95ebe chore: update generated content
  • d189d0e Revert "set repository and ghtoken attributes for gha cache type"
  • a254f8c Merge pull request #1179 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 14 +++++++------- .github/workflows/release.yml | 14 +++++++------- .github/workflows/updatecli.yml | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index bd5b886b5..9c6586761 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -35,7 +35,7 @@ jobs: run: ./build.sh pack - name: generate build provenance - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/build/output/_packages/*.nupkg" @@ -47,10 +47,10 @@ jobs: run: dotnet nuget push 'build/output/_packages/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} -s https://nuget.pkg.github.com/elastic/index.json --skip-duplicate --no-symbols - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 - name: Log in to the Elastic Container registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ${{ secrets.ELASTIC_DOCKER_REGISTRY }} username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} @@ -72,7 +72,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0 + uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -85,7 +85,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -93,12 +93,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8ad00f38..479fc2873 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,10 +42,10 @@ jobs: run: .ci/linux/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 - name: Log in to the Elastic Container registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ${{ secrets.ELASTIC_DOCKER_REGISTRY }} username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} @@ -67,7 +67,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0 + uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -80,7 +80,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -88,12 +88,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" @@ -149,7 +149,7 @@ jobs: run: ./build.bat profiler-zip - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_ZIP_FILE }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_ZIP_FILE }}" diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 7103377de..4cfa91fc5 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} From 9d91ffccbc6428b6e0efcc27f5b1ac3dff8edcc7 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 12 Aug 2024 10:27:21 +0200 Subject: [PATCH 11/77] Fix #2410 ensure we skip instrumentation of System.Web (#2411) --- src/profiler/elastic_apm_profiler/src/profiler/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs index dcbd200b3..21c4beb96 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs @@ -58,7 +58,7 @@ pub mod sig; mod startup_hook; pub mod types; -const SKIP_ASSEMBLY_PREFIXES: [&str; 22] = [ +const SKIP_ASSEMBLY_PREFIXES: [&str; 23] = [ "Elastic.Apm", "MessagePack", "Microsoft.AI", @@ -80,6 +80,7 @@ const SKIP_ASSEMBLY_PREFIXES: [&str; 22] = [ "System.Text", "System.Threading", "System.Xml", + "System.Web", "Newtonsoft", ]; const SKIP_ASSEMBLIES: [&str; 7] = [ @@ -89,7 +90,7 @@ const SKIP_ASSEMBLIES: [&str; 7] = [ "Microsoft.AspNetCore.Razor.Language", "Microsoft.AspNetCore.Mvc.RazorPages", "Anonymously Hosted DynamicMethods Assembly", - "ISymWrapper", + "ISymWrapper" ]; /// The git hash defined on build From 2d1a0db5ce437cc6042868f9247f2ce62af711b2 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 12 Aug 2024 11:45:42 +0200 Subject: [PATCH 12/77] 1.28.1 release notes (#2413) --- CHANGELOG.asciidoc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index ad9e0e69e..f587924ca 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,17 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.1]] +==== 1.28.1 - 2024/08/12 + +===== Bug fixes + +{pull}2411[#2389] Skip instrumentation of System.Web to prevent rare double configuration initialization issue + +===== Features + +{pull}2371[#2371] Global file logging, making it easier to diagnose the agent no matter the deployment type. + [[release-notes-1.28.0]] ==== 1.28.0 - 2024/07/03 From 7dfd0ea653ab8354db34034324dee312f7e52a0e Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 14 Aug 2024 13:45:24 +0200 Subject: [PATCH 13/77] Fix #2415 lookup for SqlRoleProvider breaks in ASP.NET identity 2 (#2417) --- CHANGELOG.asciidoc | 7 +++++++ .../Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index f587924ca..2ce2c9409 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,13 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.2]] +==== 1.28.2 - 2024/08/14 + +===== Bug fixes + +{pull}2415[#2415] Fixed check for SqlRoleProvider under AspNet Identity 2. + [[release-notes-1.28.1]] ==== 1.28.1 - 2024/08/12 diff --git a/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs b/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs index 26febdbf6..6654fefbe 100644 --- a/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs +++ b/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs @@ -631,7 +631,9 @@ private void FillSampledTransactionContextUser(HttpContext context, ITransaction return; var user = new User { UserName = userIdentity.Name }; - var sqlRoleProvider = System.Web.Security.Roles.Providers.Cast().Any(provider => provider.GetType().Name == "SqlRoleProvider"); + + var sqlRoleProvider = + System.Web.Security.Roles.Enabled && System.Web.Security.Roles.Providers.Cast().Any(provider => provider.GetType().Name == "SqlRoleProvider"); if (!sqlRoleProvider && context.User is ClaimsPrincipal claimsPrincipal) { try From 8e99d01ee4b839aac1587edbfc2d31eb4629def8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 15 Aug 2024 17:19:14 +0200 Subject: [PATCH 14/77] Adding filters should not force initialization of Agent. (#2418) Instead if the agent is not yet setup we simply buffer the filters until the agent gets constructed. This allows you to add filters before initializing the static agent. This manifested itself in the hosted service integrations (.NET Core) where the agent get's constructed slightly delayed. And calling ``` Agent.AddFilter((ISpan span) => { return span; }); ``` Would win the race for `Agent.Instance` over the ASP.NET core integrations. --- src/Elastic.Apm/Agent.cs | 62 ++++++++++++++++--- src/Elastic.Apm/Report/IPayloadSender.cs | 9 +++ src/Elastic.Apm/Report/PayloadSenderV2.cs | 38 ++++++++++-- .../MockPayloadSender.cs | 37 ++++++++--- .../NoopPayloadSender.cs | 11 +++- test/Elastic.Apm.Tests/FilterTests.cs | 41 +++++++++--- .../ConfigTests.cs | 10 ++- 7 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/Elastic.Apm/Agent.cs b/src/Elastic.Apm/Agent.cs index b5630b16d..e32a5ded7 100644 --- a/src/Elastic.Apm/Agent.cs +++ b/src/Elastic.Apm/Agent.cs @@ -39,7 +39,6 @@ public interface IApmAgent : IApmAgentComponents internal class ApmAgent : IApmAgent, IDisposable { internal readonly CompositeDisposable Disposables = new(); - internal ApmAgent(AgentComponents agentComponents) => Components = agentComponents ?? new AgentComponents(); internal ICentralConfigurationFetcher CentralConfigurationFetcher => Components.CentralConfigurationFetcher; @@ -72,16 +71,31 @@ public static class Agent lock (InitializationLock) { var agent = new ApmAgent(Components); - agent.Logger?.Trace() ?.Log("Initialization - Agent instance initialized. Callstack: {callstack}", new StackTrace().ToString()); + if (agent.Components.PayloadSender is not IPayloadSenderWithFilters sender) + return agent; + + ErrorFilters.ForEach(f => sender.AddFilter(f)); + TransactionFilters.ForEach(f => sender.AddFilter(f)); + SpanFilters.ForEach(f => sender.AddFilter(f)); + agent.Logger?.Trace() + ?.Log(@"Initialization - Added filters to agent (errors:{{ErrorFilters}}, transactions:{TransactionFilters} spans:{SpanFilters}", + ErrorFilters.Count, TransactionFilters.Count, SpanFilters.Count); + return agent; } }); private static readonly object InitializationLock = new object(); + private static readonly List> ErrorFilters = []; + + private static readonly List> SpanFilters = []; + + private static readonly List> TransactionFilters = []; + internal static AgentComponents Components { get; private set; } public static IConfigurationReader Config => Instance.Configuration; @@ -114,7 +128,16 @@ public static class Agent /// true if the filter was added successfully, false otherwise. In case the method /// returns false the filter won't be called. /// - public static bool AddFilter(Func filter) => CheckAndAddFilter(p => p.TransactionFilters.Add(filter)); + public static bool AddFilter(Func filter) + { + if (!IsConfigured) + { + TransactionFilters.Add(filter); + return true; + } + + return CheckAndAddFilter(p => p.AddFilter(filter)); + } /// /// Adds a filter which gets called before each span gets sent to APM Server. @@ -133,7 +156,16 @@ public static class Agent /// true if the filter was added successfully, false otherwise. In case the method /// returns false the filter won't be called. /// - public static bool AddFilter(Func filter) => CheckAndAddFilter(p => p.SpanFilters.Add(filter)); + public static bool AddFilter(Func filter) + { + if (!IsConfigured) + { + SpanFilters.Add(filter); + return true; + } + + return CheckAndAddFilter(p => p.AddFilter(filter)); + } /// /// Adds a filter which gets called before each error gets sent to APM Server. @@ -152,15 +184,23 @@ public static class Agent /// true if the filter was added successfully, false otherwise. In case the method /// returns false the filter won't be called. /// - public static bool AddFilter(Func filter) => CheckAndAddFilter(p => p.ErrorFilters.Add(filter)); + public static bool AddFilter(Func filter) + { + if (!IsConfigured) + { + ErrorFilters.Add(filter); + return true; + } + + return CheckAndAddFilter(p => p.AddFilter(filter)); + } - private static bool CheckAndAddFilter(Action action) + private static bool CheckAndAddFilter(Func action) { - if (!(Instance.PayloadSender is PayloadSenderV2 payloadSenderV2)) + if (Instance.PayloadSender is not IPayloadSenderWithFilters sender) return false; - action(payloadSenderV2); - return true; + return action(sender); } /// @@ -199,10 +239,12 @@ public static void Setup(AgentComponents agentComponents) return; } + Components ??= agentComponents; + agentComponents?.Logger?.Trace() ?.Log("Initialization - Agent.Setup called"); - Components = agentComponents; + // Force initialization var _ = LazyApmAgent.Value; } diff --git a/src/Elastic.Apm/Report/IPayloadSender.cs b/src/Elastic.Apm/Report/IPayloadSender.cs index fe97a8fc7..8533cebde 100644 --- a/src/Elastic.Apm/Report/IPayloadSender.cs +++ b/src/Elastic.Apm/Report/IPayloadSender.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Collections.Generic; using Elastic.Apm.Api; namespace Elastic.Apm.Report @@ -16,4 +18,11 @@ public interface IPayloadSender void QueueTransaction(ITransaction transaction); } + + public interface IPayloadSenderWithFilters + { + bool AddFilter(Func transactionFilter); + bool AddFilter(Func spanFilter); + bool AddFilter(Func errorFilter); + } } diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index ea94ab2c5..9c5b074b0 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -29,12 +29,12 @@ namespace Elastic.Apm.Report /// Responsible for sending the data to APM server. Implements Intake V2. /// Each instance creates its own thread to do the work. Therefore, instances should be reused if possible. /// - internal class PayloadSenderV2 : BackendCommComponentBase, IPayloadSender + internal class PayloadSenderV2 : BackendCommComponentBase, IPayloadSender, IPayloadSenderWithFilters { private const string ThisClassName = nameof(PayloadSenderV2); - internal readonly List> ErrorFilters = new List>(); - internal readonly List> SpanFilters = new List>(); - internal readonly List> TransactionFilters = new List>(); + internal readonly List> ErrorFilters = new(); + internal readonly List> SpanFilters = new(); + internal readonly List> TransactionFilters = new(); private readonly IApmServerInfo _apmServerInfo; @@ -148,6 +148,7 @@ IApmLogger logger private bool _getApmServerVersion; private bool _getCloudMetadata; + private bool _allowFilterAdd; private static readonly UTF8Encoding Utf8Encoding; private static readonly MediaTypeHeaderValue MediaTypeHeaderValue; @@ -244,6 +245,8 @@ protected override void WorkLoopIteration() } var batch = ReceiveBatch(); + if (_allowFilterAdd && batch is { Length: > 0 }) + _allowFilterAdd = false; if (batch != null) ProcessQueueItems(batch); } @@ -497,5 +500,32 @@ private T TryExecuteFilter(List> filters, T item) where T : class return item; } + + public bool AddFilter(Func transactionFilter) + { + if (!_allowFilterAdd) + return false; + + TransactionFilters.Add(transactionFilter); + return true; + } + + public bool AddFilter(Func spanFilter) + { + if (!_allowFilterAdd) + return false; + + SpanFilters.Add(spanFilter); + return true; + } + + public bool AddFilter(Func errorFilter) + { + if (!_allowFilterAdd) + return false; + + ErrorFilters.Add(errorFilter); + return true; + } } } diff --git a/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs b/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs index c72b7c21d..01df74d34 100644 --- a/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs +++ b/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs @@ -19,7 +19,7 @@ namespace Elastic.Apm.Tests.Utilities { - internal class MockPayloadSender : IPayloadSender + internal class MockPayloadSender : IPayloadSender, IPayloadSenderWithFilters { private static readonly JObject JsonSpanTypesData = JObject.Parse(File.ReadAllText(Path.Combine(SolutionPaths.Root, "test/Elastic.Apm.Tests.Utilities/TestResources/json-specs/span_types.json"))); @@ -211,7 +211,7 @@ public IReadOnlyList Metrics get { lock (_metricsLock) - return CreateImmutableSnapshot(_metrics); + return CreateImmutableSnapshot(_metrics); } } @@ -220,7 +220,7 @@ public IReadOnlyList Spans get { lock (_spanLock) - return CreateImmutableSnapshot(_spans); + return CreateImmutableSnapshot(_spans); } } @@ -229,7 +229,7 @@ public IReadOnlyList Transactions get { lock (_transactionLock) - return CreateImmutableSnapshot(_transactions); + return CreateImmutableSnapshot(_transactions); } } @@ -240,8 +240,8 @@ public void QueueError(IError error) { lock (_errorLock) { - error = _errorFilters.Aggregate(error, - (current, filter) => filter(current)); + error = _errorFilters + .Aggregate(error, (current, filter) => filter(current)); _errors.Add(error); _errorWaitHandle.Set(); } @@ -251,8 +251,8 @@ public virtual void QueueTransaction(ITransaction transaction) { lock (_transactionLock) { - transaction = _transactionFilters.Aggregate(transaction, - (current, filter) => filter(current)); + transaction = _transactionFilters + .Aggregate(transaction, (current, filter) => filter(current)); _transactions.Add(transaction); _transactionWaitHandle.Set(); } @@ -263,7 +263,8 @@ public void QueueSpan(ISpan span) VerifySpan(span); lock (_spanLock) { - span = _spanFilters.Aggregate(span, (current, filter) => filter(current)); + span = _spanFilters + .Aggregate(span, (current, filter) => filter(current)); _spans.Add(span); _spanWaitHandle.Set(); } @@ -305,6 +306,24 @@ private void VerifySpan(ISpan span) } } + public bool AddFilter(Func transactionFilter) + { + _transactionFilters.Add(transactionFilter); + return true; + } + + public bool AddFilter(Func spanFilter) + { + _spanFilters.Add(spanFilter); + return true; + } + + public bool AddFilter(Func errorFilter) + { + _errorFilters.Add(errorFilter); + return true; + } + public void QueueMetrics(IMetricSet metricSet) { lock (_metricsLock) diff --git a/test/Elastic.Apm.Tests.Utilities/NoopPayloadSender.cs b/test/Elastic.Apm.Tests.Utilities/NoopPayloadSender.cs index 7c75ce3aa..cbaaedea6 100644 --- a/test/Elastic.Apm.Tests.Utilities/NoopPayloadSender.cs +++ b/test/Elastic.Apm.Tests.Utilities/NoopPayloadSender.cs @@ -2,12 +2,14 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Collections.Generic; using Elastic.Apm.Api; using Elastic.Apm.Report; namespace Elastic.Apm.Tests.Utilities { - public class NoopPayloadSender : IPayloadSender + public class NoopPayloadSender : IPayloadSender, IPayloadSenderWithFilters { public void QueueError(IError error) { } @@ -16,5 +18,12 @@ public void QueueTransaction(ITransaction transaction) { } public void QueueSpan(ISpan span) { } public void QueueMetrics(IMetricSet metricSet) { } + + public bool AddFilter(Func transactionFilter) => true; + + public bool AddFilter(Func spanFilter) => true; + + public bool AddFilter(Func errorFilter) => true; + } } diff --git a/test/Elastic.Apm.Tests/FilterTests.cs b/test/Elastic.Apm.Tests/FilterTests.cs index 8626237a1..8de289b30 100644 --- a/test/Elastic.Apm.Tests/FilterTests.cs +++ b/test/Elastic.Apm.Tests/FilterTests.cs @@ -44,13 +44,13 @@ public async Task RenameTransactionNameAndTypeIn2Filters() => await RegisterFilterRunCodeAndAssert( payloadSender => { - payloadSender.TransactionFilters.Add(t => + payloadSender.AddFilter((ITransaction t) => { t.Name = "NewTransactionName"; return t; }); - payloadSender.TransactionFilters.Add(t => + payloadSender.AddFilter((ITransaction t) => { t.Type = "NewTransactionType"; return t; @@ -78,17 +78,17 @@ await RegisterFilterRunCodeAndAssert( payloadSender => { // Rename transaction name - payloadSender.TransactionFilters.Add(t => + payloadSender.AddFilter((ITransaction t) => { t.Name = "NewTransactionName"; return t; }); // Throw an exception - payloadSender.TransactionFilters.Add(_ => throw new Exception("This is a test exception")); + payloadSender.AddFilter((ITransaction _) => throw new Exception("This is a test exception")); // Rename transaction type - payloadSender.TransactionFilters.Add(t => + payloadSender.AddFilter((ITransaction t) => { t.Type = "NewTransactionType"; return t; @@ -115,17 +115,17 @@ public async Task FilterSpanWith3Filters() payloadSender => { // Rename span name - payloadSender.SpanFilters.Add(span => + payloadSender.AddFilter((ISpan span) => { span.Name = "NewSpanName"; return span; }); // Throw an exception - payloadSender.SpanFilters.Add(_ => throw new Exception("This is a test exception")); + payloadSender.AddFilter((ISpan _) => throw new Exception("This is a test exception")); // Rename span type - payloadSender.SpanFilters.Add(span => + payloadSender.AddFilter((ISpan span) => { span.Type = "NewSpanType"; return span; @@ -156,7 +156,7 @@ public async Task DropSpanWithSpecificName() payloadSender => { // Rename span name - payloadSender.SpanFilters.Add(span => + payloadSender.AddFilter((ISpan span) => { if (span.Name == "SpanToDrop") return null; @@ -175,7 +175,28 @@ public async Task DropSpanWithSpecificName() return true; }); - private async Task RegisterFilterRunCodeAndAssert(Action registerFilters, Action executeCodeThatGeneratesData, + /// + /// Registers a span-filter that returns false for specific span names and sends a span with that specific name. + /// Makes sure the span is not sent and serialized. + /// + [Fact] + public async Task FilterDoesNotBReak() + => await RegisterFilterRunCodeAndAssert( + payloadSender => + { + payloadSender.AddFilter((ISpan span) => span); + }, + agent => + { + agent.Tracer.CaptureTransaction("Test123", "TestTransaction", + t => { t.CaptureSpan("SpanToDrop", "TestSpan", () => Thread.Sleep(10)); }); + }, + (_, spans, _) => + { + return true; + }); + + private async Task RegisterFilterRunCodeAndAssert(Action registerFilters, Action executeCodeThatGeneratesData, Func, List, List, bool> assert ) { diff --git a/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/ConfigTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/ConfigTests.cs index 530881891..12d439c7d 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/ConfigTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/ConfigTests.cs @@ -10,6 +10,7 @@ using Elastic.Apm.AspNetCore.Tests; using Elastic.Apm.BackendComm.CentralConfig; using Elastic.Apm.Extensions.Hosting.Config; +using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; @@ -61,9 +62,11 @@ public async Task AgentDisabledInAppConfig() var capturedPayload = new MockPayloadSender(); using var agent = new ApmAgent(new TestAgentComponents( - new NoopLogger(), + new XUnitLogger(LogLevel.Trace, TestOutputHelper), new RuntimeConfigurationSnapshot(configReader))); + agent.Configuration.Enabled.Should().BeFalse(); + var client = Helper.ConfigureHttpClient(true, agent, _factory); Agent.Setup(agent); @@ -72,13 +75,14 @@ public async Task AgentDisabledInAppConfig() var isParsed = bool.TryParse(await response.Content.ReadAsStringAsync(), out var boolVal); + // Make the test fail if there was a connection to the server URL made with the sample app's url + defaultServerUrlConnectionMade.Should().BeFalse(); + isParsed.Should().BeTrue(); boolVal.Should().BeFalse(); capturedPayload.Transactions.Should().BeNullOrEmpty(); capturedPayload.Spans.Should().BeNullOrEmpty(); capturedPayload.Errors.Should().BeNullOrEmpty(); - // Make the test fail if there was a connection to the server URL made with the sample app's url - defaultServerUrlConnectionMade.Should().BeFalse(); } } } From 738792f542be9d50a3980d48052cf43686616bac Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 15 Aug 2024 17:20:50 +0200 Subject: [PATCH 15/77] Update MongoDB drivers to latest 1.28.0 (#2419) To address breaking changes https://www.mongodb.com/docs/drivers/csharp/current/upgrade/#std-label-csharp-breaking-changes-2.28.0 --- .../Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj | 2 +- .../Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj b/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj index f7917c00a..775bb6e84 100644 --- a/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj +++ b/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj @@ -26,7 +26,7 @@ - + diff --git a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj index 9113e3f48..5af0c3ce3 100644 --- a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj @@ -4,7 +4,7 @@ - + From eae543fb8de24d0830d83c7fffecb5af04876bca Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 15 Aug 2024 17:45:42 +0200 Subject: [PATCH 16/77] Release notes for 1.28.3 (#2420) --- CHANGELOG.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2ce2c9409..a187b8629 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,14 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.3]] +==== 1.28.3 - 2024/08/15 + +===== Bug fixes + +{pull}2419[#2419] Update to latest `MongoDB.Driver.Core` to address a breaking change. +{pull}2418[#2418] Adding filters should not force initialization of Agent. + [[release-notes-1.28.2]] ==== 1.28.2 - 2024/08/14 From 824a5b451e301c3a36113c2dc94ce367d3bec59f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 19 Aug 2024 13:02:54 +0200 Subject: [PATCH 17/77] Ensure we capture baggage when capturing Errors during unsampled transactions (#2427) --- src/Elastic.Apm/Model/Error.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Elastic.Apm/Model/Error.cs b/src/Elastic.Apm/Model/Error.cs index be282f45d..f2c15f8db 100644 --- a/src/Elastic.Apm/Model/Error.cs +++ b/src/Elastic.Apm/Model/Error.cs @@ -44,7 +44,7 @@ private Error(Transaction transaction, string parentId, IApmLogger loggerArg, Di ParentId = parentId; - if (transaction != null && transaction.IsSampled) + if (transaction is { IsSampled: true }) { Context = transaction.Context.DeepCopy(); @@ -55,7 +55,7 @@ private Error(Transaction transaction, string parentId, IApmLogger loggerArg, Di } } - CheckAndCaptureBaggage(); + CheckAndCaptureBaggage(transaction); IApmLogger logger = loggerArg?.Scoped($"{nameof(Error)}.{Id}"); logger.Trace() @@ -63,11 +63,15 @@ private Error(Transaction transaction, string parentId, IApmLogger loggerArg, Di this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp); } - private void CheckAndCaptureBaggage() + private void CheckAndCaptureBaggage(Transaction transaction) { if (Activity.Current == null || !Activity.Current.Baggage.Any()) return; + //if context was not set prior we set it now to ensure we capture baggage for errors + //occuring during unsampled transactions + Context ??= transaction.Context.DeepCopy(); + foreach (var baggage in Activity.Current.Baggage) { if (!WildcardMatcher.IsAnyMatch(Configuration.BaggageToAttach, baggage.Key)) From 72a5b8d96186bbbceeb6acaab2ef58a80681fcd4 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 19 Aug 2024 13:10:00 +0200 Subject: [PATCH 18/77] Ensure safer access to system.web.security.roles (#2425) Follow up from: - https://github.com/elastic/apm-agent-dotnet/pull/2377 - https://github.com/elastic/apm-agent-dotnet/pull/2417 --- .../ElasticApmModule.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs b/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs index 6654fefbe..b00e9cf44 100644 --- a/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs +++ b/src/integrations/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs @@ -632,10 +632,23 @@ private void FillSampledTransactionContextUser(HttpContext context, ITransaction var user = new User { UserName = userIdentity.Name }; - var sqlRoleProvider = - System.Web.Security.Roles.Enabled && System.Web.Security.Roles.Providers.Cast().Any(provider => provider.GetType().Name == "SqlRoleProvider"); - if (!sqlRoleProvider && context.User is ClaimsPrincipal claimsPrincipal) + FillUserIdentity(context, user); + + transaction.Context.User = user; + + _logger.Debug()?.Log("Captured user - {CapturedUser}", transaction.Context.User); + } + + private void FillUserIdentity(HttpContext context, User user) + { + try { + var sqlRoleProvider = + System.Web.Security.Roles.Enabled && System.Web.Security.Roles.Providers.Cast().Any(provider => provider.GetType().Name == "SqlRoleProvider"); + + if (sqlRoleProvider || context.User is not ClaimsPrincipal claimsPrincipal) + return; + try { static string GetClaimWithFallbackValue(ClaimsPrincipal principal, string claimType, string fallbackClaimType) @@ -652,10 +665,10 @@ static string GetClaimWithFallbackValue(ClaimsPrincipal principal, string claimT _logger.Error()?.Log("Unable to access user claims due to SqlException with message: {message}", ex.Message); } } - - transaction.Context.User = user; - - _logger.Debug()?.Log("Captured user - {CapturedUser}", transaction.Context.User); + catch (Exception ex) + { + _logger.Trace()?.Log("Error accessing System.Web.Security.Roles: {message}", ex.Message); + } } /// From b2621c8138ef79530307562140862cb17cfcac53 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 19 Aug 2024 13:44:26 +0200 Subject: [PATCH 19/77] Fix #2421 ensure _allowFilterAdd is true on initialization (#2426) --- src/Elastic.Apm/Report/PayloadSenderV2.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index 9c5b074b0..1c29bfe9b 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -32,9 +32,9 @@ namespace Elastic.Apm.Report internal class PayloadSenderV2 : BackendCommComponentBase, IPayloadSender, IPayloadSenderWithFilters { private const string ThisClassName = nameof(PayloadSenderV2); - internal readonly List> ErrorFilters = new(); - internal readonly List> SpanFilters = new(); - internal readonly List> TransactionFilters = new(); + private readonly List> ErrorFilters = new(); + private readonly List> SpanFilters = new(); + private readonly List> TransactionFilters = new(); private readonly IApmServerInfo _apmServerInfo; @@ -148,7 +148,7 @@ IApmLogger logger private bool _getApmServerVersion; private bool _getCloudMetadata; - private bool _allowFilterAdd; + private bool _allowFilterAdd = true; private static readonly UTF8Encoding Utf8Encoding; private static readonly MediaTypeHeaderValue MediaTypeHeaderValue; From b07c3215d0cf8e235d0913f0f444131f4b180800 Mon Sep 17 00:00:00 2001 From: ben-kiplot Date: Mon, 19 Aug 2024 12:44:41 +0100 Subject: [PATCH 20/77] Fix #2422 SetAgentActivationMethod throws CultureNotFoundException in global-invariant mode (#2423) Fixes #2422 --- src/Elastic.Apm/Api/Service.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Apm/Api/Service.cs b/src/Elastic.Apm/Api/Service.cs index af62d263d..058640b7a 100644 --- a/src/Elastic.Apm/Api/Service.cs +++ b/src/Elastic.Apm/Api/Service.cs @@ -84,8 +84,9 @@ private static void SetAgentActivationMethod(IApmLogger logger, Service service) { static bool CheckForLoadedAssembly(string name) { - return AppDomain.CurrentDomain.GetAssemblies().Any(n => - n.GetName().Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + // Avoid using Assembly.GetName() which can't be used with globalization-invariant mode enabled + return AppDomain.CurrentDomain.GetAssemblies() + .Any(n => n.FullName.Split(',')[0].Equals(name, StringComparison.OrdinalIgnoreCase)); } // Assume NuGet as the default. From d74c1cb88f078675510dec74bd0cdc4312b59585 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 19 Aug 2024 15:04:08 +0200 Subject: [PATCH 21/77] Release notes for 1.28.4 (#2428) --- CHANGELOG.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a187b8629..f279ef0f0 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,16 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.4]] +==== 1.28.4 - 2024/08/19 + +===== Bug fixes + +{pull}2427[#2427] Ensure we capture baggage when capturing Errors during unsampled transactions +{pull}2425[#2425] Ensure safer access to System.Web.Security.Roles +{pull}2426[#2426] Fix a bug that prevented the addition of filters to payloadsenders +{pull}2423[#2423] SetAgentActivationMethod throws CultureNotFoundException in global-invariant mode + [[release-notes-1.28.3]] ==== 1.28.3 - 2024/08/15 From 9a99a2e75d2bf875730d3b0d4ffeccf5e8ae26ce Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 28 Aug 2024 12:54:48 +0200 Subject: [PATCH 22/77] Relax ECS container ID regex (#2430) As agreed in the weekly team call, we will relax the ECS container ID regex in the spec since we have seen examples where the container ID suffix is shorter than the documented ten characters. --- CHANGELOG.asciidoc | 7 +++++++ src/Elastic.Apm/Helpers/SystemInfoHelper.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index f279ef0f0..e1c99ccf5 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,13 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.5]] +==== 1.28.5 - 2024/08/28 + +===== Bug fixes + +{pull}2430[#2430] Relax ECS container ID regex. + [[release-notes-1.28.4]] ==== 1.28.4 - 2024/08/19 diff --git a/src/Elastic.Apm/Helpers/SystemInfoHelper.cs b/src/Elastic.Apm/Helpers/SystemInfoHelper.cs index c737dbb11..b114cef9c 100644 --- a/src/Elastic.Apm/Helpers/SystemInfoHelper.cs +++ b/src/Elastic.Apm/Helpers/SystemInfoHelper.cs @@ -18,7 +18,7 @@ internal class SystemInfoHelper { private readonly Regex _containerUidRegex = new Regex("^[0-9a-fA-F]{64}$"); private readonly Regex _shortenedUuidRegex = new Regex("^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4,}"); - private readonly Regex _ecsContainerIdRegex = new Regex("^[a-z0-9]{32}-[0-9]{10}$"); + private readonly Regex _ecsContainerIdRegex = new Regex("^[a-z0-9]{32}-[0-9]{1,10}$"); private readonly Regex _podRegex = new Regex( @"(?:^/kubepods[\S]*/pod([^/]+)$)|(?:^/kubepods\.slice/(kubepods-[^/]+\.slice/)?kubepods[^/]*-pod([^/]+)\.slice$)"); From af6e14a696ef77d27254d77027b1f5e2d5ae4c34 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 9 Sep 2024 11:15:28 +0200 Subject: [PATCH 23/77] Exclude default processes and services (#2431) This also updates the comparison for excluded process and service names to be case insensitive. --- docs/setup-auto-instrumentation.asciidoc | 14 ++++++++++ .../elastic_apm_profiler/src/profiler/env.rs | 26 +++++++++++++++++-- .../elastic_apm_profiler/src/profiler/mod.rs | 4 +-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/setup-auto-instrumentation.asciidoc b/docs/setup-auto-instrumentation.asciidoc index 9bff72f74..a618b5e5a 100644 --- a/docs/setup-auto-instrumentation.asciidoc +++ b/docs/setup-auto-instrumentation.asciidoc @@ -392,12 +392,26 @@ For example, `dotnet.exe;powershell.exe`. Can be used in scenarios where profile environment variables have a global scope that would end up auto-instrumenting applications that should not be. +The following processes are _*always*_ excluded from profiling by default. + +* powershell.exe +* ServerManager.exe +* ReportingServicesService.exe +* RSHostingService.exe +* RSMananagement.exe +* RSPortal.exe +* RSConfigTool.exe + `ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES` _(optional)_:: A semi-colon separated list of APM service names to exclude from auto-instrumentation. Values defined are checked against the value of <> environment variable. +The following service names are _*always*_ excluded from profiling by default. + +* SQLServerReportingServices + `ELASTIC_OTEL_LOG_LEVEL` _(optional)_:: The log level at which the profiler should log. Valid values are diff --git a/src/profiler/elastic_apm_profiler/src/profiler/env.rs b/src/profiler/elastic_apm_profiler/src/profiler/env.rs index 5538a4db8..f69d14570 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/env.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/env.rs @@ -53,6 +53,22 @@ const ELASTIC_APM_LOG_DIRECTORY_ENV_VAR: &str = "ELASTIC_APM_LOG_DIRECTORY"; const ELASTIC_APM_SERVICE_NAME_ENV_VAR: &str = "ELASTIC_APM_SERVICE_NAME"; +// These are opinionated defaults for processes that should be excluded from profiling. +const DEFAULT_EXCLUDED_PROCESSES: &[&str] = &[ + "powershell.exe", + "ServerManager.exe", + "ReportingServicesService.exe", + "RSHostingService.exe", + "RSMananagement.exe", + "RSPortal.exe", + "RSConfigTool.exe" +]; + +// These are opinionated defaults for services that should be excluded from profiling. +const DEFAULT_EXCLUDED_SERVICE_NAMES: &[&str] = &[ + "SQLServerReportingServices" // We have reports that this "breaks" when running in IIS with global profiling enabled +]; + pub static ELASTIC_APM_PROFILER_LOG_IL: Lazy = Lazy::new(|| read_bool_env_var(ELASTIC_APM_PROFILER_LOG_IL_ENV_VAR, false)); @@ -134,11 +150,17 @@ fn read_semicolon_separated_env_var(key: &str) -> Option> { } pub fn get_exclude_processes() -> Option> { - read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR) + let mut processes = read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR) + .unwrap_or_else(Vec::new); + processes.extend(DEFAULT_EXCLUDED_PROCESSES.iter().map(|s| s.to_string())); + Some(processes) } pub fn get_exclude_service_names() -> Option> { - read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR) + let mut services = read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR) + .unwrap_or_else(Vec::new); + services.extend(DEFAULT_EXCLUDED_SERVICE_NAMES.iter().map(|s| s.to_string())); + Some(services) } pub fn get_service_name() -> Option { diff --git a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs index 21c4beb96..d767e2c05 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs @@ -522,7 +522,7 @@ impl Profiler { if let Some(exclude_process_names) = env::get_exclude_processes() { for exclude_process_name in exclude_process_names { - if process_file_name == exclude_process_name { + if process_file_name.to_lowercase() == exclude_process_name.to_lowercase() { log::info!( "Initialize: process name {} matches excluded name {}. Profiler disabled", &process_file_name, @@ -536,7 +536,7 @@ impl Profiler { if let Some(exclude_service_names) = env::get_exclude_service_names() { if let Some(service_name) = env::get_service_name() { for exclude_service_name in exclude_service_names { - if service_name == exclude_service_name { + if service_name.to_lowercase() == exclude_service_name.to_lowercase() { log::info!( "Initialize: service name {} matches excluded name {}. Profiler disabled", &service_name, From becb5dffebcf4f89b75616b996b782acda3a1317 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 10 Sep 2024 17:21:03 +0200 Subject: [PATCH 24/77] Bump upload-artifact to v4 (#2438) --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 64ed6fbe5..116913987 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -170,7 +170,7 @@ jobs: - name: Store test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-iis path: build/output/junit-*.xml From 369b4a857f411fc09aa4b6a1efffd8213dba07c6 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 11 Sep 2024 09:57:50 +0200 Subject: [PATCH 25/77] Ensure isEnabled = false won't start worker loop (#2436) Inside PayloadSenderV2, we also now early exit when events get queued. Removed early exit from constructor, allow readonly fields to be initialized so that when we start adding nullable annotations not all of them have to be nullable --- src/Elastic.Apm/Report/PayloadSenderV2.cs | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index 1c29bfe9b..5898fa340 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -58,6 +58,7 @@ internal class PayloadSenderV2 : BackendCommComponentBase, IPayloadSender, IPayl private readonly ElasticVersion _brokenActivationMethodVersion; private readonly string _cachedActivationMethod; + private readonly bool _isEnabled; public PayloadSenderV2( IApmLogger logger, @@ -73,14 +74,14 @@ public PayloadSenderV2( ) : base(isEnabled, logger, ThisClassName, service, configuration, httpMessageHandler) { - if (!isEnabled) - return; - _logger = logger?.Scoped(ThisClassName + (dbgName == null ? "" : $" (dbgName: `{dbgName}')")); + _isEnabled = isEnabled; + if (!_isEnabled) + _logger?.Debug()?.Log($"{nameof(PayloadSenderV2)} is not enabled, work loop won't be started."); + _payloadItemSerializer = new PayloadItemSerializer(); _configuration = configuration; _brokenActivationMethodVersion = new ElasticVersion(8, 7, 0); - _intakeV2EventsAbsoluteUrl = BackendCommUtils.ApmServerEndpoints.BuildIntakeV2EventsAbsoluteUrl(configuration.ServerUrl); System = system; @@ -88,12 +89,15 @@ public PayloadSenderV2( _cloudMetadataProviderCollection = new CloudMetadataProviderCollection(configuration.CloudProvider, _logger, environmentVariables); _apmServerInfo = apmServerInfo ?? new ApmServerInfo(); _serverInfoCallback = serverInfoCallback; + var process = ProcessInformation.Create(); _metadata = new Metadata { Service = service, System = System, Process = process }; foreach (var globalLabelKeyValue in configuration.GlobalLabels) _metadata.Labels.Add(globalLabelKeyValue.Key, globalLabelKeyValue.Value); _cachedActivationMethod = _metadata.Service?.Agent.ActivationMethod; - ResetActivationMethodIfKnownBrokenApmServer(); + + if (_isEnabled) + ResetActivationMethodIfKnownBrokenApmServer(); if (configuration.MaxQueueEventCount < configuration.MaxBatchEventCount) { @@ -123,7 +127,9 @@ public PayloadSenderV2( _eventQueue = new BatchBlock(configuration.MaxBatchEventCount); SetUpFilters(TransactionFilters, SpanFilters, ErrorFilters, apmServerInfo, logger); - StartWorkLoop(); + + if (_isEnabled) + StartWorkLoop(); } internal static void SetUpFilters( @@ -175,6 +181,9 @@ protected override void Dispose(bool disposing) internal async Task EnqueueEventInternal(object eventObj, string dbgEventKind) { + if (!_isEnabled) + return true; + // Enforce _maxQueueEventCount manually instead of using BatchBlock's BoundedCapacity // because of the issue of Post returning false when TriggerBatch is in progress. For more details see // https://stackoverflow.com/questions/35626955/unexpected-behaviour-tpl-dataflow-batchblock-rejects-items-while-triggerbatch @@ -217,8 +226,12 @@ internal async Task EnqueueEventInternal(object eventObj, string dbgEventK return true; } - internal void EnqueueEvent(object eventObj, string dbgEventKind) => + internal void EnqueueEvent(object eventObj, string dbgEventKind) + { + if (!_isEnabled) + return; Task.Run(async () => await EnqueueEventInternal(eventObj, dbgEventKind)); + } /// /// Runs on the background thread dedicated to sending data to APM Server. It's ok to block this thread. From db3c93da72495f548d9b4568071fdee32c126b46 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 11 Sep 2024 10:28:51 +0200 Subject: [PATCH 26/77] Release notes for 1.28.6 (#2439) --- CHANGELOG.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e1c99ccf5..6ab992a80 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,14 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.28.6]] +==== 1.28.6 - 2024/09/11 + +===== Bug fixes + +{pull}2431[#2431] Hard exclude several system processes from being auto instrumented. +{pull}2436[#2436] Disabling the agent should not try to enqueue events, now a NOOP. + [[release-notes-1.28.5]] ==== 1.28.5 - 2024/08/28 From 005a38c883539f413544641783a1bffca08adbc1 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 12 Sep 2024 11:44:47 +0200 Subject: [PATCH 27/77] Add test-reporter workflow again (#2440) Followed up from https://github.com/elastic/apm-agent-dotnet/pull/2438 This can be enabled again since we migrated to v4 --- .github/workflows/test-reporter.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/test-reporter.yml diff --git a/.github/workflows/test-reporter.yml b/.github/workflows/test-reporter.yml new file mode 100644 index 000000000..89dc5740e --- /dev/null +++ b/.github/workflows/test-reporter.yml @@ -0,0 +1,22 @@ +on: + workflow_run: + workflows: + - jan-junit-test + types: + - completed + +permissions: + contents: read + actions: read + checks: write + +jobs: + report: + runs-on: ubuntu-latest + steps: + - uses: elastic/oblt-actions/test-report@v1 + with: + artifact: /test-results(.*)/ + name: 'Test Repot $1' + path: "**/*.xml" + reporter: java-junit From 44820f41e8d4ee238725468985c59aabbea57d97 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 12 Sep 2024 13:07:33 +0200 Subject: [PATCH 28/77] ci: fix trigger (#2441) --- .github/workflows/test-reporter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-reporter.yml b/.github/workflows/test-reporter.yml index 89dc5740e..2400f5cf0 100644 --- a/.github/workflows/test-reporter.yml +++ b/.github/workflows/test-reporter.yml @@ -1,7 +1,7 @@ on: workflow_run: workflows: - - jan-junit-test + - test-windows types: - completed From 479ee083c1d873692a6f7f4e3922ef7711979a4c Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 17 Sep 2024 06:13:38 +0100 Subject: [PATCH 29/77] No longer parse request cookies, but ensure they are still redacted in the `Cookie` header string (#2444) We've identified that sending transactions with cookies parsed to `Request.Cookies` can result in failures to index those transactions when any of the cookies include periods (which is common for ASP.NET core by default). This PR removes the extraction from the header onto `Request.Cookies`. Instead, we now modify the `Cookie` header value to redact the cookie items based on the configured list of field names to sanitize. For .NET targets, we optimize this to avoid allocations as much as possible. Unfortunately, for the .NET Framework, we can't be as efficient. --- docs/configuration.asciidoc | 3 +- src/Elastic.Apm/Api/Request.cs | 7 - .../Filters/CookieHeaderRedactionFilter.cs | 91 ++++++++ .../Filters/RequestCookieExtractionFilter.cs | 53 ----- src/Elastic.Apm/Filters/Sanitization.cs | 3 - src/Elastic.Apm/Helpers/CookieHeaderParser.cs | 108 --------- .../Helpers/CookieHeaderRedacter.cs | 205 ++++++++++++++++++ src/Elastic.Apm/Report/PayloadSenderV2.cs | 4 +- .../HelpersTests/CookieHeaderParserTests.cs | 61 ------ .../CookieHeaderRedactionFilterTests.cs | 148 +++++++++++++ ...NamesTests.cs => RedactFieldNamesTests.cs} | 10 +- .../SanitizeFieldNamesTests.cs | 7 +- 12 files changed, 452 insertions(+), 248 deletions(-) create mode 100644 src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs delete mode 100644 src/Elastic.Apm/Filters/RequestCookieExtractionFilter.cs delete mode 100644 src/Elastic.Apm/Helpers/CookieHeaderParser.cs create mode 100644 src/Elastic.Apm/Helpers/CookieHeaderRedacter.cs delete mode 100644 test/Elastic.Apm.Tests/HelpersTests/CookieHeaderParserTests.cs create mode 100644 test/Elastic.Apm.Tests/HelpersTests/CookieHeaderRedactionFilterTests.cs rename test/iis/Elastic.Apm.AspNetFullFramework.Tests/{SanitizeFieldNamesTests.cs => RedactFieldNamesTests.cs} (74%) diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index eb3d9678b..84fe6f1f0 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -469,8 +469,7 @@ See https://www.owasp.org/index.php/Information_exposure_through_query_strings_i *`Cookie` header sanitization:* The `Cookie` header is automatically redacted for incoming HTTP request transactions. Each name-value pair from the -Cookie header is parsed by the agent and sent to the APM Server. Before the name-value pairs are recorded, they are -sanitized based on the `SanitizeFieldNames` configuration. Cookies with sensitive data in +cookie list is parsed by the agent and sanitized based on the `SanitizeFieldNames` configuration. Cookies with sensitive data in their value can be redacted by adding the cookie's name to the comma-separated list. [options="header"] diff --git a/src/Elastic.Apm/Api/Request.cs b/src/Elastic.Apm/Api/Request.cs index 608682c50..b06e2ef91 100644 --- a/src/Elastic.Apm/Api/Request.cs +++ b/src/Elastic.Apm/Api/Request.cs @@ -27,11 +27,6 @@ public class Request /// public Dictionary Headers { get; set; } - /// - /// This field is sanitized by a filter - /// - public Dictionary Cookies { get; set; } - [JsonProperty("http_version")] [MaxLength] public string HttpVersion { get; set; } @@ -47,8 +42,6 @@ internal Request DeepCopy() var newItem = (Request)MemberwiseClone(); if (Headers != null) newItem.Headers = Headers.ToDictionary(entry => entry.Key, entry => entry.Value); - if (Cookies != null) - newItem.Cookies = Cookies.ToDictionary(entry => entry.Key, entry => entry.Value); newItem.Socket = Socket?.DeepCopy(); newItem.Url = Url?.DeepCopy(); diff --git a/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs b/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs new file mode 100644 index 000000000..ed81382fb --- /dev/null +++ b/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections; +using System.Collections.Generic; +using Elastic.Apm.Api; +using Elastic.Apm.Config; +using Elastic.Apm.Helpers; +using Elastic.Apm.Model; +#if NET6_0_OR_GREATER +using System.Buffers; +#endif + +namespace Elastic.Apm.Filters +{ + /// + /// Redacts items from the cookie list of the Cookie request header. + /// + internal class CookieHeaderRedactionFilter + { + private const string CookieHeader = "Cookie"; + + public static IError Filter(IError error) + { + if (error is Error e && e.Context is not null) + HandleCookieHeader(e.Context?.Request?.Headers, e.Configuration.SanitizeFieldNames); + return error; + } + + public static ITransaction Filter(ITransaction transaction) + { + if (transaction is Transaction { IsContextCreated: true }) + HandleCookieHeader(transaction.Context?.Request?.Headers, transaction.Configuration.SanitizeFieldNames); + return transaction; + } + + // internal for testing + internal static void HandleCookieHeader(Dictionary headers, IReadOnlyList sanitizeFieldNames) + { + if (headers is not null) + { + // Realistically, this should be more than enough for all sensible scenarios + // e.g. Cookies | cookies | COOKIES + const int maxKeys = 4; + +#if NET6_0_OR_GREATER + var matchedKeys = ArrayPool.Shared.Rent(maxKeys); + var matchedValues = ArrayPool.Shared.Rent(maxKeys); +#else + var matchedKeys = new string[maxKeys]; + var matchedValues = new string[maxKeys]; +#endif + var matchedCount = 0; + + foreach (var header in headers) + { + if (matchedCount == maxKeys) + break; + + if (header.Key.Equals(CookieHeader, StringComparison.OrdinalIgnoreCase)) + { + matchedKeys[matchedCount] = header.Key; + matchedValues[matchedCount] = CookieHeaderRedacter.Redact(header.Value, sanitizeFieldNames); + matchedCount++; + } + } + + var replacedCount = 0; + + foreach (var headerKey in matchedKeys) + { + if (replacedCount == matchedCount) + break; + + if (headerKey is not null) + { + headers[headerKey] = matchedValues[replacedCount]; + replacedCount++; + } + } + +#if NET6_0_OR_GREATER + ArrayPool.Shared.Return(matchedKeys); + ArrayPool.Shared.Return(matchedValues); +#endif + } + } + } +} diff --git a/src/Elastic.Apm/Filters/RequestCookieExtractionFilter.cs b/src/Elastic.Apm/Filters/RequestCookieExtractionFilter.cs deleted file mode 100644 index 92ec639de..000000000 --- a/src/Elastic.Apm/Filters/RequestCookieExtractionFilter.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Elastic.Apm.Api; -using Elastic.Apm.Helpers; -using Elastic.Apm.Model; - -namespace Elastic.Apm.Filters -{ - /// - /// Extracts cookies from the Cookie request header and sets the Cookie header to [REDACTED]. - /// - internal class RequestCookieExtractionFilter - { - private static readonly WildcardMatcher[] CookieMatcher = [new WildcardMatcher.VerbatimMatcher("Cookie", true)]; - - public static IError Filter(IError error) - { - if (error is Error realError) - HandleCookieHeader(realError.Context); - return error; - } - - public static ITransaction Filter(ITransaction transaction) - { - if (transaction is Transaction { IsContextCreated: true }) - HandleCookieHeader(transaction.Context); - return transaction; - } - - private static void HandleCookieHeader(Context context) - { - if (context?.Request?.Headers is not null) - { - string matchedKey = null; - foreach (var key in context.Request.Headers.Keys) - { - if (WildcardMatcher.IsAnyMatch(CookieMatcher, key)) - { - var cookies = context.Request.Headers[key]; - context.Request.Cookies = CookieHeaderParser.ParseCookies(cookies); - matchedKey = key; - } - } - - if (matchedKey is not null) - context.Request.Headers[matchedKey] = Consts.Redacted; - } - } - } -} diff --git a/src/Elastic.Apm/Filters/Sanitization.cs b/src/Elastic.Apm/Filters/Sanitization.cs index 7f7c98fed..83dec05c9 100644 --- a/src/Elastic.Apm/Filters/Sanitization.cs +++ b/src/Elastic.Apm/Filters/Sanitization.cs @@ -18,9 +18,6 @@ public static void SanitizeHeadersInContext(Context context, IConfiguration conf if (context?.Request?.Headers is not null) RedactMatches(context?.Request?.Headers, configuration); - if (context?.Request?.Cookies is not null) - RedactMatches(context?.Request?.Cookies, configuration); - if (context?.Response?.Headers is not null) RedactMatches(context?.Response?.Headers, configuration); diff --git a/src/Elastic.Apm/Helpers/CookieHeaderParser.cs b/src/Elastic.Apm/Helpers/CookieHeaderParser.cs deleted file mode 100644 index cc378bf1c..000000000 --- a/src/Elastic.Apm/Helpers/CookieHeaderParser.cs +++ /dev/null @@ -1,108 +0,0 @@ -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; - -namespace Elastic.Apm.Helpers; - -internal static class CookieHeaderParser -{ - public static Dictionary ParseCookies(string cookieHeader) - { - // Implementation notes: - // This method handles a cookie header value for both ASP.NET (classic) and - // ASP.NET Core. As a result it must handle two possible formats. In ASP.NET - // (classic) the string is the actual Cookie value as sent over the wire, conforming - // to the HTTP standards. This uses the semicolon separator and a space between - // entries. For ASP.NET Core, when we parse the headers, we convert from the - // StringValues by calling ToString. This results in each entry being separated - // by a regular colon and no space. - - if (string.IsNullOrEmpty(cookieHeader)) - return null; - - var cookies = new Dictionary(); - -#if NETFRAMEWORK - // The use of `Span` in NETFX can cause runtime assembly loading issues due to our friend, - // binding redirects. For now, we take a slightly less allocation efficient route here, rather - // than risk introducing runtime issues for consumers. Technically, this should be "fixed" in - // NET472+, but during testing I surprisingly still reproduced an exception. Elastic.APM depends on - // `System.Diagnostics.DiagnosticSource 5.0.0` which itself depends on `System.Runtime.CompilerServices.Unsafe` - // which is where the exception occurs. 5.0.0 is marked as deprecated so we could look to prefer - // a new version but we have special handling for the ElasticApmAgentStartupHook - // zip file version. For now, we decided not to mess with this as it's hard to test all scenarios. - - var cookieValues = cookieHeader.Split(',', ';'); - - foreach (var cookieValue in cookieValues) - { - var trimmed = cookieValue.Trim(); - var parts = trimmed.Split('='); - - // Fow now, we store only the first value for a given key. This aligns to our nodeJS agent behavior. - if (parts.Length == 2 - && !string.IsNullOrEmpty(parts[0]) - && !cookies.ContainsKey(parts[0]) - && !string.IsNullOrEmpty(parts[1])) - { - cookies.Add(parts[0], parts[1]); - } - } - - return cookies; -#else - var span = cookieHeader.AsSpan(); - - while (span.Length > 0) - { - var foundComma = true; - var separatorIndex = span.IndexOfAny(',', ';'); - - if (separatorIndex == -1) - { - foundComma = false; - separatorIndex = span.Length; - } - - var entry = span.Slice(0, separatorIndex); - - var equalsIndex = entry.IndexOf('='); - - if (equalsIndex > -1) - { - var key = entry.Slice(0, equalsIndex); - var value = entry.Slice(equalsIndex + 1); - - var keyString = key.ToString(); - var valueString = value.ToString(); - - // Fow now, we store only the first value for a given key. This aligns to our nodeJS agent behavior. -#if NETSTANDARD2_0 - if (!string.IsNullOrEmpty(keyString) && !cookies.ContainsKey(keyString) && !string.IsNullOrEmpty(valueString)) - cookies.Add(keyString, valueString); -#else - if (!string.IsNullOrEmpty(keyString) && !string.IsNullOrEmpty(valueString)) - cookies.TryAdd(keyString, valueString); -#endif - } - - span = span.Slice(foundComma ? separatorIndex + 1 : span.Length); - - // skip any white space between the separator and the next entry - while (span.Length > 0) - { - if (span[0] != ' ') - break; - - span = span.Slice(1); - } - } - - return cookies; -#endif - } -} - diff --git a/src/Elastic.Apm/Helpers/CookieHeaderRedacter.cs b/src/Elastic.Apm/Helpers/CookieHeaderRedacter.cs new file mode 100644 index 000000000..174ed78bb --- /dev/null +++ b/src/Elastic.Apm/Helpers/CookieHeaderRedacter.cs @@ -0,0 +1,205 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; +#if NETFRAMEWORK +using System.Text; +#else +using System; +using System.Buffers; +#endif + +namespace Elastic.Apm.Helpers; + +internal static class CookieHeaderRedacter +{ +#if !NETFRAMEWORK + private static ReadOnlySpan Redacted => Consts.Redacted.AsSpan(); + private static ReadOnlySpan SemiColon => [';']; + private static ReadOnlySpan Comma => [',']; +#endif + + // Implementation notes: + // This method handles a cookie header value for both ASP.NET (classic) and + // ASP.NET Core. As a result it must handle two possible formats. In ASP.NET + // (classic) the string is the actual Cookie value as sent over the wire, conforming + // to the HTTP standards. This uses the semicolon separator and a space between + // entries. For ASP.NET Core, when the headers are stored into the `Request.Headers` + // on our APM data model, multiple headers of the same name, are concatenated into a + // comma-separated list. When redacting, we preserve this separation. + +#if NETFRAMEWORK + public static string Redact(string cookieHeaderValue, IReadOnlyList namesToSanitize) + { + // The use of `Span` in NETFX can cause runtime assembly loading issues due to our friend, + // binding redirects. For now, we take a slightly less allocation efficient route here, rather + // than risk introducing runtime issues for consumers. Technically, this should be "fixed" in + // NET472+, but during testing I surprisingly still reproduced an exception. Elastic.APM depends on + // `System.Diagnostics.DiagnosticSource 5.0.0` which itself depends on `System.Runtime.CompilerServices.Unsafe` + // which is where the exception occurs. 5.0.0 is marked as deprecated so we could look to prefer + // a new version but we have special handling for the ElasticApmAgentStartupHook + // zip file version. For now, we decided not to mess with this as it's hard to test all scenarios. + + if (string.IsNullOrEmpty(cookieHeaderValue)) + return string.Empty; + + var sb = new StringBuilder(cookieHeaderValue.Length); + var cookieHeaderEntries = cookieHeaderValue.Split(','); + + var requiresComma = false; + foreach (var entry in cookieHeaderEntries) + { + if (requiresComma) + sb.Append(','); + + var cookieValues = entry.Split(';'); + + var requiresSemiColon = false; + foreach (var cookieValue in cookieValues) + { + var parts = cookieValue.Split('='); + + if (requiresSemiColon) + sb.Append(';'); + + if (parts.Length == 1) + { + sb.Append(parts[0]); + } + else + { + if (WildcardMatcher.IsAnyMatch(namesToSanitize, parts[0].Trim())) + { + sb.Append(parts[0]); + sb.Append('='); + sb.Append(Consts.Redacted); + } + else + { + sb.Append(cookieValue); + } + } + + requiresSemiColon = true; + } + + requiresComma = true; + } + + return sb.ToString(); + } +#else + public static string Redact(string cookieHeaderValue, IReadOnlyList namesToSanitize) + { + if (string.IsNullOrEmpty(cookieHeaderValue)) + return null; + + var redactedCookieHeader = string.Empty; + var cookieHeaderValueSpan = cookieHeaderValue.AsSpan(); + + var written = 0; + var redactedBufferArray = ArrayPool.Shared.Rent(cookieHeaderValueSpan.Length); + var redactedBuffer = redactedBufferArray.AsSpan(); + + var whitespaceCount = 0; + while (cookieHeaderValueSpan.Length > 0) + { + // We first split on commas, to handle cases where we've combined, + // multiple headers of the same key into a concatened string. + var foundComma = true; + var commaIndex = cookieHeaderValueSpan.IndexOf(Comma); + + if (commaIndex == -1) // If there are no more separators, + { + foundComma = false; + commaIndex = cookieHeaderValueSpan.Length; + } + + var cookieHeaderEntrySpan = cookieHeaderValueSpan.Slice(0, commaIndex); + + if (written > 0) + { + Comma.CopyTo(redactedBuffer.Slice(written)); + written += Comma.Length; + } + + var writeSemiColon = false; + // Next handle each cookie item in the cookie header value + while (cookieHeaderEntrySpan.Length > 0) + { + var foundSemiColon = true; + var semiColonIndex = cookieHeaderEntrySpan.IndexOf(SemiColon); + + if (semiColonIndex == -1) // If there are no more separators, + { + foundSemiColon = false; + semiColonIndex = cookieHeaderEntrySpan.Length; + } + + var cookieItem = cookieHeaderEntrySpan.Slice(0, semiColonIndex); + + var equalsIndex = cookieItem.IndexOf('='); + + if (equalsIndex > -1) + { + if (writeSemiColon) + { + SemiColon.CopyTo(redactedBuffer.Slice(written)); + written += SemiColon.Length; + + for (var i = 0; i < whitespaceCount; i++) + { + redactedBuffer[written++] = ' '; + } + } + + var key = cookieItem.Slice(0, equalsIndex); + var value = cookieItem.Slice(equalsIndex + 1); + + key.CopyTo(redactedBuffer.Slice(written)); + written += key.Length; + + redactedBuffer.Slice(written++)[0] = '='; + + if (WildcardMatcher.IsAnyMatch(namesToSanitize, key.ToString())) + { + Redacted.CopyTo(redactedBuffer.Slice(written)); + written += Redacted.Length; + } + else + { + value.CopyTo(redactedBuffer.Slice(written)); + written += value.Length; + } + } + else + { + cookieItem.CopyTo(redactedBuffer.Slice(written)); + written += cookieItem.Length; + } + + writeSemiColon = true; + cookieHeaderEntrySpan = cookieHeaderEntrySpan.Slice(foundSemiColon ? semiColonIndex + 1 : cookieItem.Length); + + whitespaceCount = 0; + + while (cookieHeaderEntrySpan.Length > 0) + { + if (cookieHeaderEntrySpan[0] != ' ') + break; + + cookieHeaderEntrySpan = cookieHeaderEntrySpan.Slice(1); + whitespaceCount++; + } + } + + cookieHeaderValueSpan = cookieHeaderValueSpan.Slice(foundComma ? commaIndex + 1 : commaIndex); + } + + redactedCookieHeader = redactedBuffer.Slice(0, written).ToString(); + ArrayPool.Shared.Return(redactedBufferArray); + return redactedCookieHeader; + } +#endif +} diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index 5898fa340..691550730 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -141,14 +141,14 @@ IApmLogger logger ) { transactionFilters.Add(TransactionIgnoreUrlsFilter.Filter); - transactionFilters.Add(RequestCookieExtractionFilter.Filter); + transactionFilters.Add(CookieHeaderRedactionFilter.Filter); transactionFilters.Add(HeaderDictionarySanitizerFilter.Filter); // with this, stack trace demystification and conversion to the intake API model happens on a non-application thread: spanFilters.Add(new SpanStackTraceCapturingFilter(logger, apmServerInfo).Filter); errorFilters.Add(ErrorContextSanitizerFilter.Filter); - errorFilters.Add(RequestCookieExtractionFilter.Filter); + errorFilters.Add(CookieHeaderRedactionFilter.Filter); errorFilters.Add(HeaderDictionarySanitizerFilter.Filter); } diff --git a/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderParserTests.cs b/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderParserTests.cs deleted file mode 100644 index 588b4b2f2..000000000 --- a/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderParserTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Collections.Generic; -using FluentAssertions; -using Xunit; - -namespace Elastic.Apm.Tests.HelpersTests; - -public class CookieHeaderParserTests -{ - [Theory] - [MemberData(nameof(TestData))] - public void ParsesHeadersCorrectly(string input, Dictionary expected) - { - var result = Helpers.CookieHeaderParser.ParseCookies(input); - - if (expected is null) - { - result.Should().BeNull(); - return; - } - - result.Count.Should().Be(expected.Count); - result.Should().Contain(expected); - } - - public static IEnumerable TestData() => [ - [null, null], - ["", null], - ["Key1=Value1", new Dictionary() { { "Key1", "Value1" }}], - [ "Key1=Value1,Key2=Value2", new Dictionary() - { - { "Key1", "Value1" }, - { "Key2", "Value2" } - }], - [ "Key1=Value1,Key2=Value2,Key3=Value3", new Dictionary() - { - { "Key1", "Value1" }, - { "Key2", "Value2" }, - { "Key3", "Value3" } - }], - [ "Key1=Value1; Key2=Value2", new Dictionary() - { - { "Key1", "Value1" }, - { "Key2", "Value2" } - }], - [ "Key1=Value1; Key2=Value2; Key3=Value3", new Dictionary() - { - { "Key1", "Value1" }, - { "Key2", "Value2" }, - { "Key3", "Value3" } - }], - [ "Key1=Value1; Key2=Value2; Key1=Value3", new Dictionary() - { - { "Key1", "Value1" }, - { "Key2", "Value2" } - }], - ]; -} diff --git a/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderRedactionFilterTests.cs b/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderRedactionFilterTests.cs new file mode 100644 index 000000000..ec6c6916d --- /dev/null +++ b/test/Elastic.Apm.Tests/HelpersTests/CookieHeaderRedactionFilterTests.cs @@ -0,0 +1,148 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; +using Elastic.Apm.Config; +using Elastic.Apm.Filters; +using Elastic.Apm.Helpers; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Tests.HelpersTests; + +public class CookieHeaderRedactionFilterTests +{ + [Theory] + [MemberData(nameof(TestData))] + public void RedactsHeadersCorrectly(Dictionary headers, IReadOnlyList sanitizeFieldNames, Dictionary expectedHeaders) + { + CookieHeaderRedactionFilter.HandleCookieHeader(headers, sanitizeFieldNames); + + if (expectedHeaders is null) + { + headers.Should().BeNull(); + } + else + { + headers.Should().Contain(expectedHeaders); + } + } + + public static TheoryData, IReadOnlyList, Dictionary> TestData() => + new() + { + // When the input is null, the output should also be null + { null, ConfigConsts.DefaultValues.SanitizeFieldNames, null }, + + // Standard Cookie header, with a single cookie item whose name does not match the redaction list + { + new Dictionary() { ["Cookie"] = "Cookie1=Value1" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1=Value1" } + }, + + // Standard Cookie header, with a multiple cookie items whose names do not match the redaction list + { + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Cookie2=Value2" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Cookie2=Value2" } + }, + + // In the unlikely case that a request includes multiple cookie headers, + // including some using non-standard casing, we expect each one to be + // returned. + { + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1", + ["cookie"] = "Cookie2=Value2", + ["COOKIE"] = "Cookie3=Value3" + }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1", + ["cookie"] = "Cookie2=Value2", + ["COOKIE"] = "Cookie3=Value3" + } + }, + + // If the request contained multiple `Cookie` headers, our code will store each value + // on the `Context.Request.Headers` dictionary, concatenated by commas. + { + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1,Cookie2=Value2" + }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1,Cookie2=Value2" + } + }, + + // If the request contained multiple `Cookie` headers, our code will store each value + // on the `Context.Request.Headers` dictionary, concatenated by commas. In this scenario, + // one of the headers includes multiple cookie items + { + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1,Cookie2=Value2; Cookie3=Value3" + }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() + { + ["Cookie"] = "Cookie1=Value1,Cookie2=Value2; Cookie3=Value3" + } + }, + + // Standard Cookie header, with a single cookie item whose name matches the redaction list + { + new Dictionary() { ["Cookie"] = "Authorization=Bearer ABC123" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Authorization=[REDACTED]" } + }, + + // Standard Cookie header, with a multiple cookie items, one of which has a name + // matching the redaction list + { + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Authorization=Bearer ABC123" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Authorization=[REDACTED]" } + }, + + // If the request contained multiple `Cookie` headers, our code will store each value + // on the `Context.Request.Headers` dictionary, concatenated by commas. In this scenario, + // one of the headers includes a cookie item that should be redacted. + { + new Dictionary() { ["Cookie"] = "Authorization=Bearer ABC123,Cookie1=Value1" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Authorization=[REDACTED],Cookie1=Value1" } + }, + + // An example where we have multiple `Cookie` headers, some with multiple items, some of which + // require redaction. + { + new Dictionary() { ["Cookie"] = "Cookie1=Value1,Cookie2=Value2; Authorization=Bearer ABC123,Cookie3=Value3; password=thisissecret!" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1=Value1,Cookie2=Value2; Authorization=[REDACTED],Cookie3=Value3; password=[REDACTED]" } + }, + + // MALFORMED EXAMPLES + + // This example includes an invalid cookie entry. We expect to preserve this as-is and cannot redact it. + { + new Dictionary() { ["Cookie"] = "Cookie1" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1" } + }, + + // This example includes non-standard whitespace. We expect to preserve this as-is. + { + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Cookie2=Value2" }, + ConfigConsts.DefaultValues.SanitizeFieldNames, + new Dictionary() { ["Cookie"] = "Cookie1=Value1; Cookie2=Value2" } + } + }; +} diff --git a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/SanitizeFieldNamesTests.cs b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/RedactFieldNamesTests.cs similarity index 74% rename from test/iis/Elastic.Apm.AspNetFullFramework.Tests/SanitizeFieldNamesTests.cs rename to test/iis/Elastic.Apm.AspNetFullFramework.Tests/RedactFieldNamesTests.cs index 63f167d93..603a8a939 100644 --- a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/SanitizeFieldNamesTests.cs +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/RedactFieldNamesTests.cs @@ -10,9 +10,9 @@ namespace Elastic.Apm.AspNetFullFramework.Tests { [Collection(Consts.AspNetFullFrameworkTestsCollection)] - public class SanitizeFieldNamesTests : TestsBase + public class RedactFieldNamesTests : TestsBase { - public SanitizeFieldNamesTests(ITestOutputHelper xUnitOutputHelper) + public RedactFieldNamesTests(ITestOutputHelper xUnitOutputHelper) : base(xUnitOutputHelper) { } @@ -42,12 +42,8 @@ await WaitAndCustomVerifyReceivedData(receivedData => receivedData.Transactions.Should().ContainSingle(); receivedData.Transactions[0].Context.Should().NotBeNull(); receivedData.Transactions[0].Context.Request.Should().NotBeNull(); - receivedData.Transactions[0].Context.Request.Cookies.Should().NotBeNull(); - receivedData.Transactions[0].Context.Request.Cookies.Should().NotBeNull(); - receivedData.Transactions[0].Context.Request.Headers["Cookie"].Should().Be(Apm.Consts.Redacted); - receivedData.Transactions[0].Context.Request.Cookies["MySecureCookie"].Should().Be(Apm.Consts.Redacted); - receivedData.Transactions[0].Context.Request.Cookies["SafeCookie"].Should().Be("This is safe to record and should not be redacted."); + receivedData.Transactions[0].Context.Request.Headers["Cookie"].Should().Be($"password={Apm.Consts.Redacted}; SafeCookie=This is safe to record and should not be redacted."); }, false); } } diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/SanitizeFieldNamesTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/SanitizeFieldNamesTests.cs index e30113b6b..16bbff946 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/SanitizeFieldNamesTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/SanitizeFieldNamesTests.cs @@ -460,7 +460,7 @@ public async Task CustomWithRequestBodyNoError(string sanitizeFieldNames, string } [Fact] - public async Task SanitizesCookieHeaders() + public async Task RedactCookieHeaders() { CreateAgent("set-cookie, mysecurecookie"); @@ -483,12 +483,9 @@ public async Task SanitizesCookieHeaders() _capturedPayload.Transactions.Should().ContainSingle(); _capturedPayload.FirstTransaction.Context.Should().NotBeNull(); _capturedPayload.FirstTransaction.Context.Request.Should().NotBeNull(); - _capturedPayload.FirstTransaction.Context.Request.Cookies.Should().NotBeNull(); _capturedPayload.FirstTransaction.Context.Response.Should().NotBeNull(); - _capturedPayload.FirstTransaction.Context.Request.Headers["Cookie"].Should().Be(Apm.Consts.Redacted); - _capturedPayload.FirstTransaction.Context.Request.Cookies["MySecureCookie"].Should().Be(Apm.Consts.Redacted); - _capturedPayload.FirstTransaction.Context.Request.Cookies["SafeCookie"].Should().Be("123"); + _capturedPayload.FirstTransaction.Context.Request.Headers["Cookie"].Should().Be($"MySecureCookie={Apm.Consts.Redacted}; SafeCookie=123"); } } } From 31cb8cb6a9f065435838231cfcf098bc670f93e1 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 18 Sep 2024 10:41:35 +0100 Subject: [PATCH 30/77] Release notes for 1.29.0 (#2447) As titled --- CHANGELOG.asciidoc | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6ab992a80..9b5d7a681 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,29 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.29.0]] +==== 1.29.0 - 2024/09/18 + +This release includes a breaking change in how we parse and send transaction cookies. In 1.26.0, we +introduced improved cookie redaction based on the `SanitizeFieldNames` configuration. To implement this, +we extracted each cookie from the `Cookie` header, storing them in a cookie dictionary on the transaction request data. +We have identified a problem with the storage of cookies that include period characters due to the mapping of such data +when stored in the APM data stream. This behaviour can lead to lost transactions on requests which include such cookies. +This is common in ASP.NET Core due to the default cookie names used for sessions, authentication, etc. + +In this release, we no longer parse out individual cookies, and the cookie `Dictionary` has been removed from the +data model. This means that cookies will no longer be indexed individually. However, we have ensured that we +retain the primary reason for the earlier change, which was to redact the values of sensitive cookies. Any cookies with +a name matching the `SanitizeFieldNames` patterns will be redacted in the value of the `Cookie` header +we store. + +For most consumers, we expect the impact to be minimal. However, if you were relying on the parsed cookie +fields, adjustments will be necessary to work with the `Cookie` header value instead. + +===== Breaking changes + +{pull}2444[#2444] No longer parse request cookies, but ensure they are still redacted in the `Cookie` header string + [[release-notes-1.28.6]] ==== 1.28.6 - 2024/09/11 From 624a2493903329de06f700cea7280afdb8ea8ad1 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Wed, 18 Sep 2024 16:04:50 +0200 Subject: [PATCH 31/77] github-action: use oblt-actions/git/setup (#2442) --- .github/workflows/bootstrap/action.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bootstrap/action.yml b/.github/workflows/bootstrap/action.yml index 88c6f40a0..92fffabe7 100644 --- a/.github/workflows/bootstrap/action.yml +++ b/.github/workflows/bootstrap/action.yml @@ -60,7 +60,7 @@ runs: echo "major-version=$(echo ${AGENT_VERSION} | cut -d"." -f1)" >> $GITHUB_OUTPUT # Setup git config - - uses: elastic/apm-pipeline-library/.github/actions/setup-git@current + - uses: elastic/oblt-actions/git/setup@v1 # install common dependencies - name: Install common dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 479fc2873..9a096a7bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -174,7 +174,7 @@ jobs: fetch-depth: 0 - name: Setup git config - uses: elastic/apm-pipeline-library/.github/actions/setup-git@current + uses: elastic/oblt-actions/git/setup@v1 - name: Create GitHub Pull Request if minor release. run: | From 9b41c9d066a054ef53205d8239a23a261c53bc09 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Wed, 18 Sep 2024 16:27:30 +0200 Subject: [PATCH 32/77] github-action: use ephemeral tokens with the required permissions (#2437) --- .github/workflows/updatecli.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 4cfa91fc5..4f7219c5e 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -17,6 +17,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Get token + id: get_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + with: + app_id: ${{ secrets.OBS_AUTOMATION_APP_ID }} + private_key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} + permissions: >- + { + "contents": "write", + "pull_requests": "write" + } + - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io @@ -27,13 +39,13 @@ jobs: with: command: --experimental compose diff env: - GITHUB_TOKEN: ${{ secrets.UPDATECLI_GH_TOKEN }} + GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose apply env: - GITHUB_TOKEN: ${{ secrets.UPDATECLI_GH_TOKEN }} + GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - if: failure() uses: elastic/oblt-actions/slack/send@v1 From 0de006cb9854d60e172863893a34739e1f2478bf Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 26 Sep 2024 21:56:19 +0200 Subject: [PATCH 33/77] github-action: use elastic/oblt-actions/github/is-member-of (#2453) --- .github/workflows/labeler.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 71a3e6fe2..85de98621 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,10 +17,11 @@ jobs: steps: - id: is_elastic_member - uses: elastic/apm-pipeline-library/.github/actions/is-member-elastic-org@current + uses: elastic/oblt-actions/github/is-member-of@v1 with: - username: ${{ github.actor }} - token: ${{ secrets.APM_TECH_USER_TOKEN }} + github-org: "elastic" + github-user: ${{ github.actor }} + github-token: ${{ secrets.APM_TECH_USER_TOKEN }} - name: Add community and triage labels if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' From 5277fd0a5ba13523b4b9bbe24baff298e4cfc411 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:57:26 +0200 Subject: [PATCH 34/77] Bump the github-actions group across 1 directory with 4 updates (#2451) Bumps the github-actions group with 4 updates in the / directory: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/build-push-action](https://github.com/docker/build-push-action) and [azure/login](https://github.com/azure/login). --- .github/workflows/release-main.yml | 12 ++++++------ .github/workflows/release.yml | 12 ++++++------ .github/workflows/test-linux.yml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 9c6586761..4c94bcd25 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -35,7 +35,7 @@ jobs: run: ./build.sh pack - name: generate build provenance - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/build/output/_packages/*.nupkg" @@ -47,7 +47,7 @@ jobs: run: dotnet nuget push 'build/output/_packages/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} -s https://nuget.pkg.github.com/elastic/index.json --skip-duplicate --no-symbols - name: Set up Docker Buildx - uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -72,7 +72,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -85,7 +85,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -93,12 +93,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a096a7bf..2e98c976d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: run: .ci/linux/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -67,7 +67,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -80,7 +80,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -88,12 +88,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" @@ -149,7 +149,7 @@ jobs: run: ./build.bat profiler-zip - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_ZIP_FILE }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_ZIP_FILE }}" diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 4cde21905..c44a0772d 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -97,7 +97,7 @@ jobs: azure: 'true' - name: 'Az CLI login' - uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1 + uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0 with: client-id: ${{ secrets.ARM_CLIENT_ID }} tenant-id: ${{ secrets.ARM_TENANT_ID }} From 5b9434437506ee77a5fda981e30679fd76c555b6 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 4 Oct 2024 13:27:28 +0100 Subject: [PATCH 35/77] Improve OTel bridge compatibility with existing Azure instrumentation (#2455) This ensures that the Azure SDK instrumentation doesn't use the existing `Activity` span ID, which, when combined with the OTel bridge, can lead to spans being parented by themselves. This also enhances the logic of the OTel bridge so that we skip span creation for activities coming from the Azure SDK libraries if our Azure instrumentation assemblies are loaded. This ensures we don't create essentially repeated child spans. Fixes #2454 --- .../DiagnosticListeners/KnownListeners.cs | 8 ++-- src/Elastic.Apm/Model/Transaction.cs | 29 +++++++---- .../OpenTelemetry/ElasticActivityListener.cs | 38 ++++++++++++++- ...reMessagingServiceBusDiagnosticListener.cs | 5 +- .../ActivityIntegrationTests.cs | 48 +++++++++++++------ ...zureQueueStorageDiagnosticListenerTests.cs | 16 +++++++ .../SqlServerFixture.cs | 16 ++++++- .../AdoNet/SqlServerFixture.cs | 19 +++++++- 8 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/Elastic.Apm/DiagnosticListeners/KnownListeners.cs b/src/Elastic.Apm/DiagnosticListeners/KnownListeners.cs index 69126a476..3711ed0c7 100644 --- a/src/Elastic.Apm/DiagnosticListeners/KnownListeners.cs +++ b/src/Elastic.Apm/DiagnosticListeners/KnownListeners.cs @@ -9,18 +9,18 @@ namespace Elastic.Apm.DiagnosticListeners { internal static class KnownListeners { + // Known activity names public const string MicrosoftAspNetCoreHostingHttpRequestIn = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; public const string SystemNetHttpHttpRequestOut = "System.Net.Http.HttpRequestOut"; public const string SystemNetHttpDesktopHttpRequestOut = "System.Net.Http.Desktop.HttpRequestOut"; public const string ApmTransactionActivityName = "ElasticApm.Transaction"; - - public static HashSet KnownListenersList => new() - { + public static HashSet SkippedActivityNamesSet => + [ MicrosoftAspNetCoreHostingHttpRequestIn, SystemNetHttpHttpRequestOut, SystemNetHttpDesktopHttpRequestOut, ApmTransactionActivityName - }; + ]; } } diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 680b167e2..9280a7809 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -1,5 +1,4 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. +// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information @@ -27,6 +26,17 @@ internal class Transaction : ITransaction { internal static readonly string ApmTransactionActivityName = "ElasticApm.Transaction"; +#if NET + internal static readonly ActivitySource ElasticApmActivitySource = new("Elastic.Apm"); + + // This simply ensures our transaction activity is always created. + internal static readonly ActivityListener Listener = new() + { + ShouldListenTo = s => s.Name == ElasticApmActivitySource.Name, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData + }; +#endif + internal readonly TraceState _traceState; internal readonly ConcurrentDictionary SpanTimings = new(); @@ -144,7 +154,6 @@ internal Transaction( (configuration.TraceContinuationStrategy == ConfigConsts.SupportedValues.RestartExternal && (distributedTracingData?.TraceState == null || distributedTracingData is { TraceState: { SampleRate: null } })); - // For each new transaction, start an Activity if we're not ignoring them. // If Activity.Current is not null, the started activity will be a child activity, // so the traceid and tracestate of the parent will flow to it. @@ -154,7 +163,7 @@ internal Transaction( if (current != null) _activity = current; - // Otherwise we will start an activity explicitly and ensure it trace_id and trace_state respect our bookkeeping. + // Otherwise we will start an activity explicitly and ensure its trace_id and trace_state respect our bookkeeping. // Unless explicitly asked not to through `ignoreActivity`: (https://github.com/elastic/apm-agent-dotnet/issues/867#issuecomment-650170150) else if (!ignoreActivity) _activity = StartActivity(shouldRestartTrace); @@ -539,16 +548,20 @@ internal void SetOutcome(Outcome outcome) _outcome = outcome; } - private Activity StartActivity(bool shouldRestartTrace) + private static Activity StartActivity(bool shouldRestartTrace) { +#if NET + var activity = ElasticApmActivitySource.CreateActivity(KnownListeners.ApmTransactionActivityName, ActivityKind.Internal); +#else var activity = new Activity(KnownListeners.ApmTransactionActivityName); +#endif if (shouldRestartTrace) { - activity.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), + activity?.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), Activity.Current != null ? Activity.Current.ActivityTraceFlags : ActivityTraceFlags.None); } - activity.SetIdFormat(ActivityIdFormat.W3C); - activity.Start(); + activity?.SetIdFormat(ActivityIdFormat.W3C); + activity?.Start(); return activity; } diff --git a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs index 4751834a0..0ebbe1fdc 100644 --- a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs +++ b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs @@ -38,6 +38,14 @@ public class ElasticActivityListener : IDisposable internal ElasticActivityListener(IApmAgent agent, HttpTraceConfiguration httpTraceConfiguration) => (_logger, _httpTraceConfiguration) = (agent.Logger?.Scoped(nameof(ElasticActivityListener)), httpTraceConfiguration); + private static readonly bool HasServiceBusInstrumentation = + AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => + assembly.GetName().Name == "Elastic.Apm.Azure.ServiceBus") != null; + + private static readonly bool HasStorageInstrumentation = + AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => + assembly.GetName().Name == "Elastic.Apm.Azure.Storage") != null; + private readonly IApmLogger _logger; private Tracer _tracer; private readonly HttpTraceConfiguration _httpTraceConfiguration; @@ -64,8 +72,34 @@ internal void Start(Tracer tracerInternal) private Action ActivityStarted => activity => { - if (KnownListeners.KnownListenersList.Contains(activity.DisplayName)) + // If the Elastic instrumentation for ServiceBus is present, we skip duplicating the instrumentation through the OTel bridge. + // Without this, we end up with some redundant spans in the trace with subtle differences. + if (HasServiceBusInstrumentation && activity.Tags.Any(kvp => + kvp.Key.Equals("az.namespace", StringComparison.Ordinal) && kvp.Value.Equals("Microsoft.ServiceBus", StringComparison.Ordinal))) + { + _logger.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.ServiceBus' " + + "activity because 'Elastic.Apm.Azure.ServiceBus' is present in the application.", + activity.DisplayName, activity.Id, activity.TraceId); + + return; + } + + if (HasStorageInstrumentation && activity.Tags.Any(kvp => + kvp.Key.Equals("az.namespace", StringComparison.Ordinal) && kvp.Value.Equals("Microsoft.Storage", StringComparison.Ordinal))) + { + _logger.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.Storage' " + + "activity because 'Elastic.Apm.Azure.Storage' is present in the application.", + activity.DisplayName, activity.Id, activity.TraceId); + return; + } + + if (KnownListeners.SkippedActivityNamesSet.Contains(activity.DisplayName)) + { + _logger.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped because it matched " + + "a skipped activity name defined in KnownListeners."); + return; + } _logger.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId}", activity.DisplayName, activity.Id, activity.TraceId); @@ -150,7 +184,7 @@ private void CreateSpanForActivity(Activity activity, long timestamp, List kv, string action) _onMessageCurrent = currentSegment switch { - Span span => span.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant(), - id: activity.SpanId.ToString()), + Span span => span.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant()), Transaction transaction => transaction.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, - action.ToLowerInvariant(), id: activity.SpanId.ToString()), + action.ToLowerInvariant()), _ => _onMessageCurrent }; } diff --git a/test/Elastic.Apm.Tests/ActivityIntegrationTests.cs b/test/Elastic.Apm.Tests/ActivityIntegrationTests.cs index 86870c850..fe232020e 100644 --- a/test/Elastic.Apm.Tests/ActivityIntegrationTests.cs +++ b/test/Elastic.Apm.Tests/ActivityIntegrationTests.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; using System.Diagnostics; using System.Linq; using System.Threading; @@ -11,7 +10,6 @@ using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; using Xunit; -using Xunit.Abstractions; namespace Elastic.Apm.Tests { @@ -26,21 +24,42 @@ public class ActivityIntegrationTests [Fact] public void ElasticTransactionReusesTraceIdFromCurrentActivity() { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; +#if NET + var listener = new ActivityListener + { + ShouldListenTo = a => a.Name == "Elastic.Apm", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => { }, + ActivityStopped = activity => { } + }; - var activity = new Activity("UnitTestActivity"); - activity.Start(); + ActivitySource.AddActivityListener(listener); +#endif - Activity.Current.TraceId.Should().Be(activity.TraceId); + try + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; - var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) - agent.Tracer.CaptureTransaction("TestTransaction", "Test", () => Thread.Sleep(10)); + var activity = new Activity("UnitTestActivity"); + activity.Start(); - payloadSender.FirstTransaction.TraceId.Should().Be(activity.TraceId.ToString()); - payloadSender.FirstTransaction.ParentId.Should().BeNullOrEmpty(); + Activity.Current.TraceId.Should().Be(activity.TraceId); - activity.Stop(); + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + agent.Tracer.CaptureTransaction("TestTransaction", "Test", () => Thread.Sleep(10)); + + payloadSender.FirstTransaction.TraceId.Should().Be(activity.TraceId.ToString()); + payloadSender.FirstTransaction.ParentId.Should().BeNullOrEmpty(); + + activity.Stop(); + } + catch + { +#if NET + listener.Dispose(); +#endif + } } /// @@ -147,7 +166,8 @@ public void MultipleTransactionInOneActivity() payloadSender.Transactions[0].Id.Should().NotBe(payloadSender.Transactions[1].Id); activity.Stop(); } -#if NET5_0_OR_GREATER + +#if NET /// /// Makes sure that transactions on the same Activity are part of the same trace. /// @@ -188,8 +208,6 @@ public async Task ActivityRespectsSampling() var sampledSpans = payloadSender.Spans.Where(t => t.IsSampled).ToArray(); sampledSpans.Length.Should().Be(sampled.Length); - - } #endif } diff --git a/test/azure/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs b/test/azure/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs index d7d9510f7..c4ce7a416 100644 --- a/test/azure/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs +++ b/test/azure/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Azure.Storage.Queues; using Elastic.Apm.Api; @@ -21,6 +22,21 @@ public class AzureQueueStorageDiagnosticListenerTests : IDisposable private readonly IDisposable _subscription; private readonly ITestOutputHelper _output; +#if NET + static AzureQueueStorageDiagnosticListenerTests() + { + var listener = new ActivityListener + { + ShouldListenTo = a => a.Name == "Elastic.Apm", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => { }, + ActivityStopped = activity => { } + }; + + ActivitySource.AddActivityListener(listener); + } +#endif + public AzureQueueStorageDiagnosticListenerTests(AzureStorageTestEnvironment environment, ITestOutputHelper output) { var logger = new XUnitLogger(LogLevel.Trace, output); diff --git a/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs b/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs index 2095543c0..1563fe387 100644 --- a/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs +++ b/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs @@ -3,6 +3,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Runtime.InteropServices; using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; @@ -22,8 +23,19 @@ public sealed class SqlServerFixture : IAsyncLifetime public SqlServerFixture(IMessageSink sink) { _sink = sink; - _container = new MsSqlBuilder() - .Build(); + + // see: https://blog.rufer.be/2024/09/22/workaround-fix-testcontainers-sql-error-docker-dotnet-dockerapiexception-docker-api-responded-with-status-codeconflict/ + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") + .Build(); + } + else + { + _container = new MsSqlBuilder() + .Build(); + } } public async Task InitializeAsync() diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs index b3d7b040c..3f25f8dcf 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs @@ -3,6 +3,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Runtime.InteropServices; using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; @@ -14,7 +15,23 @@ public sealed class SqlServerCollection : ICollectionFixture { public sealed class SqlServerFixture : IAsyncLifetime { - private readonly MsSqlContainer _container = new MsSqlBuilder().Build(); + private readonly MsSqlContainer _container; + + public SqlServerFixture() + { + // see: https://blog.rufer.be/2024/09/22/workaround-fix-testcontainers-sql-error-docker-dotnet-dockerapiexception-docker-api-responded-with-status-codeconflict/ + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") + .Build(); + } + else + { + _container = new MsSqlBuilder() + .Build(); + } + } public string ConnectionString => _container.GetConnectionString(); From af6d159a421c6d1905161de1712892d253c4e2cd Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 4 Oct 2024 22:17:17 +0100 Subject: [PATCH 36/77] Revert skipping of System.Web (#2457) We thought this fixed [this bug](https://github.com/elastic/apm-agent-dotnet/issues/2410), but it prevents profiler loading, so we are reverting for now. --- src/profiler/elastic_apm_profiler/src/profiler/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs index d767e2c05..53b20d1f7 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/mod.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/mod.rs @@ -58,7 +58,7 @@ pub mod sig; mod startup_hook; pub mod types; -const SKIP_ASSEMBLY_PREFIXES: [&str; 23] = [ +const SKIP_ASSEMBLY_PREFIXES: [&str; 22] = [ "Elastic.Apm", "MessagePack", "Microsoft.AI", @@ -80,8 +80,7 @@ const SKIP_ASSEMBLY_PREFIXES: [&str; 23] = [ "System.Text", "System.Threading", "System.Xml", - "System.Web", - "Newtonsoft", + "Newtonsoft" ]; const SKIP_ASSEMBLIES: [&str; 7] = [ "mscorlib", @@ -1061,7 +1060,7 @@ impl Profiler { ); log::trace!("ModuleLoadFinished: tracking {} module(s)", modules.len()); - + if call_target_enabled { let rejit_count = self.calltarget_request_rejit_for_module(module_id, module_metadata)?; From fb8e5494175e4924ab362a74a218031de07f7ad3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 8 Oct 2024 12:12:37 +0200 Subject: [PATCH 37/77] Limit attribute count and truncate values in ElasticActivityListener (#2461) Implement limits to comply with OpenTelemetry specs, capping attributes at 128 and truncating string values to 10,000 characters. This ensures payload size remains manageable and adheres to the defined standards. https://opentelemetry.io/docs/specs/otel/common/#attribute-limits --- .../OpenTelemetry/ElasticActivityListener.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs index 0ebbe1fdc..d045e8ceb 100644 --- a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs +++ b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs @@ -228,9 +228,20 @@ private static void UpdateOTelAttributes(Activity activity, OTel otel) { if (!activity.TagObjects.Any()) return; + // https://opentelemetry.io/docs/specs/otel/common/#attribute-limits + // copy max 128 keys and truncate values to 10k chars (the current maximum for e.g. statement.db). + var i = 0; otel.Attributes ??= new Dictionary(); foreach (var (key, value) in activity.TagObjects) - otel.Attributes[key] = value; + { + if (i >= 128) break; + + if (value is string s) + otel.Attributes[key] = s.Truncate(10_000); + else + otel.Attributes[key] = value; + i++; + } } private static void UpdateSpan(Activity activity, Span span) From 3ffe9b4f24f2e3cfdc5e82c1a42e346d64d36fb9 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 10 Oct 2024 09:25:50 +0200 Subject: [PATCH 38/77] github-actions: use ephemeral tokens (#2462) --- .github/workflows/addToProject.yml | 17 ++++++++++++++--- .github/workflows/labeler.yml | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/addToProject.yml b/.github/workflows/addToProject.yml index a9da0f9de..149d77044 100644 --- a/.github/workflows/addToProject.yml +++ b/.github/workflows/addToProject.yml @@ -11,10 +11,21 @@ jobs: if: github.event.issue && github.event.issue.milestone runs-on: ubuntu-latest steps: + - name: Get token + id: get_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + with: + app_id: ${{ secrets.OBS_AUTOMATION_APP_ID }} + private_key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} + permissions: >- + { + "organization_projects": "write", + "issues": "read" + } - name: Get project data env: - GITHUB_TOKEN: ${{ secrets.APM_TECH_USER_TOKEN }} + GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} TEAM: .NET ORGANIZATION: elastic PROJECT_NUMBER: 595 @@ -50,7 +61,7 @@ jobs: - name: Add issue to project env: - GITHUB_TOKEN: ${{ secrets.APM_TECH_USER_TOKEN }} + GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} ISSUE_ID: ${{ github.event.issue.node_id }} run: | item_id="$( gh api graphql -f query=' @@ -66,7 +77,7 @@ jobs: - name: Set fields env: - GITHUB_TOKEN: ${{ secrets.APM_TECH_USER_TOKEN }} + GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} run: | gh api graphql -f query=' mutation ( diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 85de98621..97001eb79 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -15,13 +15,23 @@ jobs: triage: runs-on: ubuntu-latest steps: - + - name: Get token + id: get_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + with: + app_id: ${{ secrets.OBS_AUTOMATION_APP_ID }} + private_key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} + permissions: >- + { + "members": "read" + } + - id: is_elastic_member uses: elastic/oblt-actions/github/is-member-of@v1 with: github-org: "elastic" github-user: ${{ github.actor }} - github-token: ${{ secrets.APM_TECH_USER_TOKEN }} + github-token: ${{ steps.get_token.outputs.token }} - name: Add community and triage labels if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' From 655fea0964f88a59eb5633ec4940bed4f66804f8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 10 Oct 2024 19:35:08 +0200 Subject: [PATCH 39/77] Add IntakeResponse deserialization for detailed error logging (#2460) Introduced the `IntakeResponse` class to better handle and log errors from APM server responses. Updated `PayloadSenderV2` to use this class for extracting error messages and logging them in a more informative way. Added a deserialization method in `PayloadItemSerializer` for converting response streams. --- src/Elastic.Apm/Report/IntakeResponse.cs | 25 +++++++++++++++++++ src/Elastic.Apm/Report/PayloadSenderV2.cs | 17 +++++++++++-- .../Serialization/PayloadItemSerializer.cs | 8 ++++++ test/Elastic.Apm.Tests/LoggerTests.cs | 2 +- 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/Elastic.Apm/Report/IntakeResponse.cs diff --git a/src/Elastic.Apm/Report/IntakeResponse.cs b/src/Elastic.Apm/Report/IntakeResponse.cs new file mode 100644 index 000000000..32389b91a --- /dev/null +++ b/src/Elastic.Apm/Report/IntakeResponse.cs @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; +using Elastic.Apm.Libraries.Newtonsoft.Json; + +namespace Elastic.Apm.Report; + +internal class IntakeResponse +{ + [JsonProperty("accepted")] + public int Accepted { get; set; } + + [JsonProperty("errors")] + public IReadOnlyCollection Errors { get; set; } +} + +internal class IntakeError +{ + + [JsonProperty("message")] + public string Message { get; set; } +} diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index 691550730..7c2b189e7 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -420,14 +421,26 @@ private void ProcessQueueItems(object[] queueItems) // ReSharper disable ConditionIsAlwaysTrueOrFalse if (response is null || !response.IsSuccessStatusCode) { + var message = "Unknown 400 Bad Request"; + if (response?.Content != null) + { +#if NET6_0_OR_GREATER + var intakeResponse = _payloadItemSerializer.Deserialize(response.Content.ReadAsStream()); +#else + var intakeResponse = _payloadItemSerializer.Deserialize(response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()); +#endif + if (intakeResponse.Errors.Count > 0) + message = string.Join(", ", intakeResponse.Errors.Select(e => e.Message).Distinct()); + } _logger?.Error() ?.Log("Failed sending event." + " Events intake API absolute URL: {EventsIntakeAbsoluteUrl}." + " APM Server response: status code: {ApmServerResponseStatusCode}" - + ", content: \n{ApmServerResponseContent}" + + ", reasons: {ApmServerResponseContent}" , _intakeV2EventsAbsoluteUrl.Sanitize() , response?.StatusCode, - response?.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult()); + message + ); } // ReSharper enable ConditionIsAlwaysTrueOrFalse else diff --git a/src/Elastic.Apm/Report/Serialization/PayloadItemSerializer.cs b/src/Elastic.Apm/Report/Serialization/PayloadItemSerializer.cs index 7a97f7d65..4dc973813 100644 --- a/src/Elastic.Apm/Report/Serialization/PayloadItemSerializer.cs +++ b/src/Elastic.Apm/Report/Serialization/PayloadItemSerializer.cs @@ -42,6 +42,14 @@ internal T Deserialize(string json) return val ?? default; } + internal T Deserialize(Stream stream) + { + using var sr = new StreamReader(stream); + using var jsonTextReader = new JsonTextReader(sr); + var val = _serializer.Deserialize(jsonTextReader); + return val; + } + /// /// Serializes the item to JSON /// diff --git a/test/Elastic.Apm.Tests/LoggerTests.cs b/test/Elastic.Apm.Tests/LoggerTests.cs index 5952a3713..10a02acb4 100644 --- a/test/Elastic.Apm.Tests/LoggerTests.cs +++ b/test/Elastic.Apm.Tests/LoggerTests.cs @@ -430,7 +430,7 @@ public void PayloadSenderNoUserNamePwPrintedForServerUrl() /// Initializes a with a server url which contains basic authentication. /// The test makes sure that the user name and password from basic auth. is not printed in the logs. /// - [Fact] + [Fact(Skip = "Flakey on CI")] public void PayloadSenderNoUserNamePwPrintedForServerUrlWithServerReturn() { var userName = "abc"; From 0539257e257b237f003a7e2413c2261408571bae Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 11 Oct 2024 10:10:29 +0100 Subject: [PATCH 40/77] Add release notes for 1.30.0 (#2463) As titled. --- CHANGELOG.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 9b5d7a681..a6c943ad0 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,16 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.30.0]] +==== 1.30.0 - 2024/10/11 + +===== Bug fixes + +{pull}2455[#2455] Improve OTel bridge compatibility with existing Azure instrumentation +{pull}2457[#2457] Revert skipping of System.Web to fix profiler-based installation +{pull}2461[#2461] Limit attribute count and truncate values in ElasticActivityListener +{pull}2460[#2460] Add IntakeResponse deserialization for detailed error logging + [[release-notes-1.29.0]] ==== 1.29.0 - 2024/09/18 From fb84dec990c4c39588083d02aee97bfb79b4dea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:13:46 +0200 Subject: [PATCH 41/77] Bump the github-actions group with 2 updates (#2459) Bumps the github-actions group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/setup-buildx-action` from 3.6.1 to 3.7.1 Updates `docker/build-push-action` from 6.7.0 to 6.9.0 --- .github/workflows/release-main.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 4c94bcd25..f88f36188 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -47,7 +47,7 @@ jobs: run: dotnet nuget push 'build/output/_packages/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} -s https://nuget.pkg.github.com/elastic/index.json --skip-duplicate --no-symbols - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -72,7 +72,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e98c976d..cc59e7865 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: run: .ci/linux/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -67,7 +67,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: cache-from: type=gha cache-to: type=gha,mode=max From ba5d3b27db1bd1c6826545692615e8be2a59dea2 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 22 Oct 2024 16:43:57 +0200 Subject: [PATCH 42/77] Move to nuget central package management (#2467) --- .github/.github.csproj | 1 + .github/workflows/bootstrap/action.yml | 2 +- .github/workflows/test-windows.yml | 3 - Directory.Build.props | 5 +- Directory.Packages.props | 141 ++++++++++++++++++ ElasticApmAgent.sln.DotSettings | 5 +- .../Elastic.Apm.Benchmarks.csproj | 6 +- .../Elastic.Apm.Profiling.csproj | 2 +- build/build.fsproj | 25 ++-- build/scripts/Build.fs | 17 +-- build/scripts/Targets.fs | 2 + sample/ApiSamples/ApiSamples.csproj | 2 +- ...Elastic.Apm.Azure.ServiceBus.Sample.csproj | 7 +- .../ElasticsearchSample.csproj | 6 +- .../HttpListenerSample.csproj | 2 +- .../Sample.Microsoft.Data.SqlClient.csproj | 5 +- .../StackExchangeRedisSample.csproj | 4 +- sample/WebApiExample/WebApiExample.csproj | 4 +- .../WorkerServiceSample.csproj | 2 +- src/Directory.Build.props | 2 +- .../Elastic.Apm.Specification.csproj | 5 +- src/Elastic.Apm/Elastic.Apm.csproj | 22 +-- .../Elastic.Apm.Azure.Functions.csproj | 8 +- .../Elastic.Apm.Elasticsearch.csproj | 2 +- .../Elastic.Apm.EntityFramework6.csproj | 8 +- .../Elastic.Apm.EntityFrameworkCore.csproj | 8 +- .../Elastic.Apm.MongoDb.csproj | 2 +- .../Elastic.Apm.StackExchange.Redis.csproj | 2 +- .../Elastic.Apm.AspNetCore.csproj | 13 +- .../Elastic.Apm.Extensions.Hosting.csproj | 20 +-- .../Elastic.Apm.Extensions.Logging.csproj | 8 +- ....Apm.Profiler.IntegrationsGenerator.csproj | 4 +- .../Elastic.Apm.Profiler.Managed.csproj | 4 +- .../Elastic.Apm.StartupHook.Loader.csproj | 2 +- .../ElasticApmAgentStartupHook.csproj | 2 +- test/Directory.Build.props | 19 +-- .../Elastic.Apm.Feature.Tests.csproj | 6 +- .../Elastic.Apm.Tests.Utilities.csproj | 10 +- .../Elastic.Apm.Tests.csproj | 4 +- .../Elastic.Apm.Azure.CosmosDb.Tests.csproj | 11 +- .../Elastic.Apm.Azure.ServiceBus.Tests.csproj | 8 +- .../Elastic.Apm.Azure.Storage.Tests.csproj | 12 +- .../Elastic.AzureFunctionApp.InProcess.csproj | 2 +- .../Elastic.AzureFunctionApp.Isolated.csproj | 6 +- .../AspNetFullFrameworkSampleApp.csproj | 26 ++-- .../AspNetFullFrameworkSampleApp/Web.config | 2 +- ...astic.Apm.AspNetFullFramework.Tests.csproj | 10 +- .../Elastic.Apm.Elasticsearch.Tests.csproj | 8 +- ...astic.Apm.EntityFrameworkCore.Tests.csproj | 8 +- .../Elastic.Apm.MongoDb.Tests.csproj | 4 +- .../Elastic.Apm.SqlClient.Tests.csproj | 11 +- ...astic.Apm.StackExchange.Redis.Tests.csproj | 8 +- ...Elastic.Clients.Elasticsearch.Tests.csproj | 4 +- .../Elastic.Apm.Grpc.Tests.csproj | 13 +- .../GrpcClientSample/GrpcClientSample.csproj | 11 +- .../GrpcServiceSample.csproj | 4 +- ...Elastic.Apm.AspNetCore.Static.Tests.csproj | 6 +- .../Elastic.Apm.AspNetCore.Tests.csproj | 8 +- ...lastic.Apm.Extensions.Hosting.Tests.csproj | 2 +- ...lastic.Apm.Extensions.Logging.Tests.csproj | 2 +- .../HostingTestApp/HostingTestApp.csproj | 2 +- .../SampleAspNetCoreApp.csproj | 26 ++-- .../SampleConsoleNetCoreApp/Program.cs | 2 +- .../SampleConsoleNetCoreApp.csproj | 2 +- .../OpenTelemetrySample.csproj | 2 +- .../AdoNet/NpgSqlCommandTests.cs | 8 +- .../Elastic.Apm.Profiler.Managed.Tests.csproj | 18 +-- .../KafkaSample/KafkaSample.csproj | 4 +- .../MySqlDataSample/MySqlDataSample.csproj | 3 +- .../NpgsqlSample/NpgsqlSample.csproj | 4 +- .../OracleManagedDataAccessCoreSample.csproj | 3 +- .../OracleManagedDataAccessSample.csproj | 3 +- .../applications/RabbitMqSample/Program.cs | 12 -- .../RabbitMqSample/RabbitMqSample.csproj | 4 +- .../SatelliteAssemblySample.csproj | 2 +- .../SqlClientSample/SqlClientSample.csproj | 2 +- .../SqliteSample/SqliteSample.csproj | 2 +- .../DotnetProject.cs | 1 + .../Elastic.Apm.StartupHook.Tests.csproj | 2 +- 79 files changed, 381 insertions(+), 279 deletions(-) create mode 100644 Directory.Packages.props diff --git a/.github/.github.csproj b/.github/.github.csproj index 568bf7d5e..4c8ecc80d 100644 --- a/.github/.github.csproj +++ b/.github/.github.csproj @@ -2,6 +2,7 @@ net8.0 False + disable diff --git a/.github/workflows/bootstrap/action.yml b/.github/workflows/bootstrap/action.yml index 92fffabe7..6dd6532b3 100644 --- a/.github/workflows/bootstrap/action.yml +++ b/.github/workflows/bootstrap/action.yml @@ -36,7 +36,7 @@ runs: - uses: actions/cache@v4 with: path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.[cf]sproj*') }} + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.[cf]sproj*') }}-${{ hashFiles('**/*.*.props') }} restore-keys: | ${{ runner.os }}-nuget diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 116913987..610314bf7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -61,9 +61,6 @@ jobs: - name: 'Tests: Unit' run: ./build.bat test --test-suite unit - - name: 'Tests: Integrations' - run: ./build.bat test --test-suite integrations - integrations-tests: runs-on: windows-2022 needs: [ 'format', 'tests' ] diff --git a/Directory.Build.props b/Directory.Build.props index 36feb4a9a..b9c67200b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,6 +22,7 @@ but dotnet pack expects to pack it e.g. Microsoft.NET.Sdk.Web test and sample projects. --> false false + true @@ -29,8 +30,4 @@ canary.0 0.1 - - - - \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..265aa2053 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,141 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ElasticApmAgent.sln.DotSettings b/ElasticApmAgent.sln.DotSettings index 14fff26b1..7e2279db6 100644 --- a/ElasticApmAgent.sln.DotSettings +++ b/ElasticApmAgent.sln.DotSettings @@ -277,7 +277,7 @@ See the LICENSE file in the project root for more information </Entry.Match> <Entry.SortBy> <Kind Is="Member" /> - <Name Is="Enter Pattern Here" /> + <Name /> </Entry.SortBy> </Entry> <Entry DisplayName="Readonly Fields"> @@ -295,7 +295,7 @@ See the LICENSE file in the project root for more information <Entry.SortBy> <Access /> <Readonly /> - <Name Is="Enter Pattern Here" /> + <Name /> </Entry.SortBy> </Entry> <Entry DisplayName="Constructors"> @@ -570,6 +570,7 @@ See the LICENSE file in the project root for more information True True True + True True True False diff --git a/benchmarks/Elastic.Apm.Benchmarks/Elastic.Apm.Benchmarks.csproj b/benchmarks/Elastic.Apm.Benchmarks/Elastic.Apm.Benchmarks.csproj index 5c8794dad..332e83070 100644 --- a/benchmarks/Elastic.Apm.Benchmarks/Elastic.Apm.Benchmarks.csproj +++ b/benchmarks/Elastic.Apm.Benchmarks/Elastic.Apm.Benchmarks.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/benchmarks/Elastic.Apm.Profiling/Elastic.Apm.Profiling.csproj b/benchmarks/Elastic.Apm.Profiling/Elastic.Apm.Profiling.csproj index 7b61bfd5f..ff4861898 100644 --- a/benchmarks/Elastic.Apm.Profiling/Elastic.Apm.Profiling.csproj +++ b/benchmarks/Elastic.Apm.Profiling/Elastic.Apm.Profiling.csproj @@ -9,7 +9,7 @@ - + diff --git a/build/build.fsproj b/build/build.fsproj index ec4305f39..4ba737fed 100644 --- a/build/build.fsproj +++ b/build/build.fsproj @@ -24,21 +24,22 @@ - - - + + - - - - - - + + + + + + - + - - + + + + diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index e6f9954fb..91bd2b372 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -8,16 +8,14 @@ open System open System.Collections.Generic open System.IO open System.IO.Compression -open System.Linq open System.Runtime.InteropServices open System.Xml.Linq -open Buildalyzer +open System.Xml.XPath open Fake.Core open Fake.DotNet open Fake.IO open Fake.IO.Globbing.Operators open Scripts.TestEnvironment -open TestEnvironment open Tooling module Build = @@ -87,15 +85,14 @@ module Build = }) projectOrSln /// Gets the current version of System.Diagnostics.DiagnosticSource referenced by Elastic.Apm - let private getCurrentApmDiagnosticSourceVersion = + let getCurrentApmDiagnosticSourceVersion = match currentDiagnosticSourceVersion with | Some v -> v - | None -> - let manager = AnalyzerManager(); - let analyzer = manager.GetProject(Paths.SrcProjFile "Elastic.Apm") - let analyzeResult = analyzer.Build("netstandard2.0").First() - let values = analyzeResult.PackageReferences.["System.Diagnostics.DiagnosticSource"] - let version = SemVer.parse values.["Version"] + | None -> + let xml = XDocument.Load("Directory.Packages.props") + let package = xml.XPathSelectElement("//PackageVersion[@Include='System.Diagnostics.DiagnosticSource']") + let version = package.Attribute("Version").Value + let version = SemVer.parse version currentDiagnosticSourceVersion <- Some(version) version diff --git a/build/scripts/Targets.fs b/build/scripts/Targets.fs index 46102e688..46cc8dc3d 100644 --- a/build/scripts/Targets.fs +++ b/build/scripts/Targets.fs @@ -99,6 +99,8 @@ module Main = Targets.Target("restore", Build.Restore) + Targets.Target("print-diagnostics-version", fun _ -> printfn $"%s{Build.getCurrentApmDiagnosticSourceVersion.ToString()}") + Targets.Target("format", Build.Format) Targets.Target("build", ["restore"; "clean"; "version"], Build.Build) diff --git a/sample/ApiSamples/ApiSamples.csproj b/sample/ApiSamples/ApiSamples.csproj index 2f47d1200..7850a8d1a 100644 --- a/sample/ApiSamples/ApiSamples.csproj +++ b/sample/ApiSamples/ApiSamples.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0 false diff --git a/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj index 431a8af51..c1578849d 100644 --- a/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj +++ b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj @@ -2,12 +2,13 @@ Exe - net5.0 + net8.0 - - + + + diff --git a/sample/ElasticsearchSample/ElasticsearchSample.csproj b/sample/ElasticsearchSample/ElasticsearchSample.csproj index 3ddc470ac..6fddcc8af 100644 --- a/sample/ElasticsearchSample/ElasticsearchSample.csproj +++ b/sample/ElasticsearchSample/ElasticsearchSample.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/sample/HttpListenerSample/HttpListenerSample.csproj b/sample/HttpListenerSample/HttpListenerSample.csproj index 9d1152725..8b6bbc6d7 100644 --- a/sample/HttpListenerSample/HttpListenerSample.csproj +++ b/sample/HttpListenerSample/HttpListenerSample.csproj @@ -10,7 +10,7 @@ - + diff --git a/sample/Sample.Microsoft.Data.SqlClient/Sample.Microsoft.Data.SqlClient.csproj b/sample/Sample.Microsoft.Data.SqlClient/Sample.Microsoft.Data.SqlClient.csproj index 37d35037a..58f4c64d9 100644 --- a/sample/Sample.Microsoft.Data.SqlClient/Sample.Microsoft.Data.SqlClient.csproj +++ b/sample/Sample.Microsoft.Data.SqlClient/Sample.Microsoft.Data.SqlClient.csproj @@ -13,9 +13,8 @@ - - - + + diff --git a/sample/StackExchangeRedisSample/StackExchangeRedisSample.csproj b/sample/StackExchangeRedisSample/StackExchangeRedisSample.csproj index 4fe4a49a1..e5519371e 100644 --- a/sample/StackExchangeRedisSample/StackExchangeRedisSample.csproj +++ b/sample/StackExchangeRedisSample/StackExchangeRedisSample.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/sample/WebApiExample/WebApiExample.csproj b/sample/WebApiExample/WebApiExample.csproj index 325a13544..3252084e8 100644 --- a/sample/WebApiExample/WebApiExample.csproj +++ b/sample/WebApiExample/WebApiExample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/sample/WorkerServiceSample/WorkerServiceSample.csproj b/sample/WorkerServiceSample/WorkerServiceSample.csproj index 7d9d055a6..b1c0cd783 100644 --- a/sample/WorkerServiceSample/WorkerServiceSample.csproj +++ b/sample/WorkerServiceSample/WorkerServiceSample.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index bd2017b18..2f23a478c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -33,6 +33,6 @@ . - + \ No newline at end of file diff --git a/src/Elastic.Apm.Specification/Elastic.Apm.Specification.csproj b/src/Elastic.Apm.Specification/Elastic.Apm.Specification.csproj index f9b12bf93..43d6863ef 100644 --- a/src/Elastic.Apm.Specification/Elastic.Apm.Specification.csproj +++ b/src/Elastic.Apm.Specification/Elastic.Apm.Specification.csproj @@ -6,8 +6,9 @@ - - + + + diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index 10345a958..ef7b4ae93 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -75,36 +75,36 @@ - + - + - - + + - - + + - + - - + + - + - + diff --git a/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj b/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj index 00b344092..d00bf90ba 100644 --- a/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj +++ b/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/src/instrumentations/Elastic.Apm.Elasticsearch/Elastic.Apm.Elasticsearch.csproj b/src/instrumentations/Elastic.Apm.Elasticsearch/Elastic.Apm.Elasticsearch.csproj index 20b0a4c32..3ceb21104 100644 --- a/src/instrumentations/Elastic.Apm.Elasticsearch/Elastic.Apm.Elasticsearch.csproj +++ b/src/instrumentations/Elastic.Apm.Elasticsearch/Elastic.Apm.Elasticsearch.csproj @@ -10,7 +10,7 @@ true - + diff --git a/src/instrumentations/Elastic.Apm.EntityFramework6/Elastic.Apm.EntityFramework6.csproj b/src/instrumentations/Elastic.Apm.EntityFramework6/Elastic.Apm.EntityFramework6.csproj index 81470557b..4205c04c1 100644 --- a/src/instrumentations/Elastic.Apm.EntityFramework6/Elastic.Apm.EntityFramework6.csproj +++ b/src/instrumentations/Elastic.Apm.EntityFramework6/Elastic.Apm.EntityFramework6.csproj @@ -9,14 +9,10 @@ true - - all - + - - all - + diff --git a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj index 9bfb5b292..cbedad1a4 100644 --- a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj +++ b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj @@ -9,14 +9,10 @@ true - - all - + - - all - + diff --git a/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj b/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj index 775bb6e84..30994d4bb 100644 --- a/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj +++ b/src/instrumentations/Elastic.Apm.MongoDb/Elastic.Apm.MongoDb.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/instrumentations/Elastic.Apm.StackExchange.Redis/Elastic.Apm.StackExchange.Redis.csproj b/src/instrumentations/Elastic.Apm.StackExchange.Redis/Elastic.Apm.StackExchange.Redis.csproj index 407c2318d..b9ea39d5e 100644 --- a/src/instrumentations/Elastic.Apm.StackExchange.Redis/Elastic.Apm.StackExchange.Redis.csproj +++ b/src/instrumentations/Elastic.Apm.StackExchange.Redis/Elastic.Apm.StackExchange.Redis.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj index cfcf7f297..245a1fe50 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj +++ b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj @@ -16,13 +16,12 @@ - - - - - - - + + + + + + diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj index 8725f6c15..96cd9a6e0 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj @@ -23,21 +23,17 @@ - - - - - all - + + + + - - - - - all - + + + + diff --git a/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj b/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj index 16707baf0..88cd762a6 100644 --- a/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj @@ -20,15 +20,11 @@ - - all - + - - all - + diff --git a/src/profiler/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj b/src/profiler/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj index 6e8cec7ad..7b9dbb773 100644 --- a/src/profiler/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj +++ b/src/profiler/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj b/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj index cbe3fc9e8..e93487e05 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj +++ b/src/profiler/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj b/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj index 60984f437..c04e0e3d4 100644 --- a/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj +++ b/src/startuphook/Elastic.Apm.StartupHook.Loader/Elastic.Apm.StartupHook.Loader.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj b/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj index f75da942b..48eec369e 100644 --- a/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj +++ b/src/startuphook/ElasticApmAgentStartupHook/ElasticApmAgentStartupHook.csproj @@ -11,6 +11,6 @@ - + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 03a58c389..2ccf826a6 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -19,18 +19,15 @@ - + - - - + + + - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + \ No newline at end of file diff --git a/test/Elastic.Apm.Feature.Tests/Elastic.Apm.Feature.Tests.csproj b/test/Elastic.Apm.Feature.Tests/Elastic.Apm.Feature.Tests.csproj index ba0e4f1e9..eab125a06 100644 --- a/test/Elastic.Apm.Feature.Tests/Elastic.Apm.Feature.Tests.csproj +++ b/test/Elastic.Apm.Feature.Tests/Elastic.Apm.Feature.Tests.csproj @@ -6,9 +6,9 @@ - - - + + + diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index 30f0746d1..f6792c4a1 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -34,11 +34,11 @@ - - - - - + + + + + diff --git a/test/Elastic.Apm.Tests/Elastic.Apm.Tests.csproj b/test/Elastic.Apm.Tests/Elastic.Apm.Tests.csproj index ec857243d..9aae67096 100644 --- a/test/Elastic.Apm.Tests/Elastic.Apm.Tests.csproj +++ b/test/Elastic.Apm.Tests/Elastic.Apm.Tests.csproj @@ -28,8 +28,8 @@ - - + + diff --git a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/Elastic.Apm.Azure.CosmosDb.Tests.csproj b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/Elastic.Apm.Azure.CosmosDb.Tests.csproj index 1dcc62ed6..3e149fc8c 100644 --- a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/Elastic.Apm.Azure.CosmosDb.Tests.csproj +++ b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/Elastic.Apm.Azure.CosmosDb.Tests.csproj @@ -7,10 +7,13 @@ - - - - + + + + + + + diff --git a/test/azure/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj b/test/azure/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj index 247a0e56a..91993cdee 100644 --- a/test/azure/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj +++ b/test/azure/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/test/azure/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj b/test/azure/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj index ac1e939cc..b9c002692 100644 --- a/test/azure/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj +++ b/test/azure/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj @@ -7,12 +7,12 @@ - - - - - - + + + + + + diff --git a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj index 640172471..8bb50ce7e 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj +++ b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj @@ -8,7 +8,7 @@ false - + diff --git a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj index 5d928890c..0523ea9ab 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj +++ b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj @@ -12,9 +12,9 @@ The versions below are NOT the latest but anything newer breaks when attempting to run func See: https://github.com/Azure/azure-functions-core-tools/issues/3594 --> - - - + + + diff --git a/test/iis/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj b/test/iis/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj index d6a6b71a4..fa30e279b 100644 --- a/test/iis/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj +++ b/test/iis/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj @@ -84,19 +84,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/test/iis/AspNetFullFrameworkSampleApp/Web.config b/test/iis/AspNetFullFrameworkSampleApp/Web.config index bb2c07310..e44d37826 100644 --- a/test/iis/AspNetFullFrameworkSampleApp/Web.config +++ b/test/iis/AspNetFullFrameworkSampleApp/Web.config @@ -302,7 +302,7 @@ - + diff --git a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj index acd5a9fc1..724da16b1 100644 --- a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj @@ -7,11 +7,11 @@ NU1702 - - - - - + + + + + TargetFramework=net462 diff --git a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/Elastic.Apm.Elasticsearch.Tests.csproj b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/Elastic.Apm.Elasticsearch.Tests.csproj index d63d5e98c..42bde9d01 100644 --- a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/Elastic.Apm.Elasticsearch.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/Elastic.Apm.Elasticsearch.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/test/instrumentations/Elastic.Apm.EntityFrameworkCore.Tests/Elastic.Apm.EntityFrameworkCore.Tests.csproj b/test/instrumentations/Elastic.Apm.EntityFrameworkCore.Tests/Elastic.Apm.EntityFrameworkCore.Tests.csproj index cc735d362..c646b4d7a 100644 --- a/test/instrumentations/Elastic.Apm.EntityFrameworkCore.Tests/Elastic.Apm.EntityFrameworkCore.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.EntityFrameworkCore.Tests/Elastic.Apm.EntityFrameworkCore.Tests.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj index 5af0c3ce3..edc9bd3ce 100644 --- a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj @@ -4,8 +4,8 @@ - - + + diff --git a/test/instrumentations/Elastic.Apm.SqlClient.Tests/Elastic.Apm.SqlClient.Tests.csproj b/test/instrumentations/Elastic.Apm.SqlClient.Tests/Elastic.Apm.SqlClient.Tests.csproj index 7c2ac0981..3f54b9a97 100644 --- a/test/instrumentations/Elastic.Apm.SqlClient.Tests/Elastic.Apm.SqlClient.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.SqlClient.Tests/Elastic.Apm.SqlClient.Tests.csproj @@ -6,15 +6,16 @@ - - - + + + - + + - + diff --git a/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/Elastic.Apm.StackExchange.Redis.Tests.csproj b/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/Elastic.Apm.StackExchange.Redis.Tests.csproj index 0d8e9c702..5acad44f8 100644 --- a/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/Elastic.Apm.StackExchange.Redis.Tests.csproj +++ b/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/Elastic.Apm.StackExchange.Redis.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/Elastic.Clients.Elasticsearch.Tests.csproj b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/Elastic.Clients.Elasticsearch.Tests.csproj index 9c689be86..5dcdb4532 100644 --- a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/Elastic.Clients.Elasticsearch.Tests.csproj +++ b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/Elastic.Clients.Elasticsearch.Tests.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/test/instrumentations/grpc/Elastic.Apm.Grpc.Tests/Elastic.Apm.Grpc.Tests.csproj b/test/instrumentations/grpc/Elastic.Apm.Grpc.Tests/Elastic.Apm.Grpc.Tests.csproj index b6f7490f2..022fc2d4f 100644 --- a/test/instrumentations/grpc/Elastic.Apm.Grpc.Tests/Elastic.Apm.Grpc.Tests.csproj +++ b/test/instrumentations/grpc/Elastic.Apm.Grpc.Tests/Elastic.Apm.Grpc.Tests.csproj @@ -6,14 +6,11 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + diff --git a/test/instrumentations/grpc/GrpcClientSample/GrpcClientSample.csproj b/test/instrumentations/grpc/GrpcClientSample/GrpcClientSample.csproj index 41fa62526..61ba9ef8a 100644 --- a/test/instrumentations/grpc/GrpcClientSample/GrpcClientSample.csproj +++ b/test/instrumentations/grpc/GrpcClientSample/GrpcClientSample.csproj @@ -2,16 +2,13 @@ Exe - netcoreapp3.1 + net8.0 - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/test/instrumentations/grpc/GrpcServiceSample/GrpcServiceSample.csproj b/test/instrumentations/grpc/GrpcServiceSample/GrpcServiceSample.csproj index 395682a05..5f152b3c0 100644 --- a/test/instrumentations/grpc/GrpcServiceSample/GrpcServiceSample.csproj +++ b/test/instrumentations/grpc/GrpcServiceSample/GrpcServiceSample.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net8.0 @@ -9,7 +9,7 @@ - + diff --git a/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Elastic.Apm.AspNetCore.Static.Tests.csproj b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Elastic.Apm.AspNetCore.Static.Tests.csproj index a8f8afd26..0a69f097f 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Elastic.Apm.AspNetCore.Static.Tests.csproj +++ b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Elastic.Apm.AspNetCore.Static.Tests.csproj @@ -3,9 +3,9 @@ net8.0 - - - + + + diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/Elastic.Apm.AspNetCore.Tests.csproj b/test/integrations/Elastic.Apm.AspNetCore.Tests/Elastic.Apm.AspNetCore.Tests.csproj index e86422f51..664509ac1 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/Elastic.Apm.AspNetCore.Tests.csproj +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/Elastic.Apm.AspNetCore.Tests.csproj @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj index d7340831f..9aed9da54 100644 --- a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj +++ b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj index 2dcd2d9b8..cbfb24f0b 100644 --- a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj +++ b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/integrations/applications/HostingTestApp/HostingTestApp.csproj b/test/integrations/applications/HostingTestApp/HostingTestApp.csproj index bff7b18bc..db9baea24 100644 --- a/test/integrations/applications/HostingTestApp/HostingTestApp.csproj +++ b/test/integrations/applications/HostingTestApp/HostingTestApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj b/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj index 5881da018..09e04e82c 100644 --- a/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj +++ b/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj @@ -6,23 +6,23 @@ AnyCPU - + - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs b/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs index 4c982a12e..1748cf5bf 100644 --- a/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs +++ b/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs @@ -25,7 +25,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureLogging((_, logging) => { logging.ClearProviders(); - logging.AddConsole(options => options.IncludeScopes = true); + logging.AddSimpleConsole(options => options.IncludeScopes = true); }); } } diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj b/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj index d3e588ae1..2000ec119 100644 --- a/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj +++ b/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/opentelemetry/OpenTelemetrySample/OpenTelemetrySample.csproj b/test/opentelemetry/OpenTelemetrySample/OpenTelemetrySample.csproj index 47874c04b..4a5e429bb 100644 --- a/test/opentelemetry/OpenTelemetrySample/OpenTelemetrySample.csproj +++ b/test/opentelemetry/OpenTelemetrySample/OpenTelemetrySample.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net8.0 Linux diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs index e24b9ef00..4e125bfa1 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs @@ -32,8 +32,10 @@ public static IEnumerable TestParameters { get { + //TODO test version 8.x + // use the version defined in NpgsqlSample - var npgSqlVersion = "5.0.7"; + var npgSqlVersion = "5.0.18"; // TODO: Add x64/x86 options. macOS and Linux do not support x86 yield return new object[] { "net8.0", npgSqlVersion }; @@ -41,10 +43,10 @@ public static IEnumerable TestParameters if (TestEnvironment.IsWindows) yield return new object[] { "net462", npgSqlVersion }; - npgSqlVersion = "6.0.2"; + npgSqlVersion = "6.0.12"; yield return new object[] { "net8.0", npgSqlVersion }; - npgSqlVersion = "7.0.2"; + npgSqlVersion = "7.0.8"; yield return new object[] { "net8.0", npgSqlVersion }; } } diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj index 5950a9c41..af086bd6d 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj @@ -14,15 +14,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/test/profiler/applications/KafkaSample/KafkaSample.csproj b/test/profiler/applications/KafkaSample/KafkaSample.csproj index a2f3c7cdf..3f2b25136 100644 --- a/test/profiler/applications/KafkaSample/KafkaSample.csproj +++ b/test/profiler/applications/KafkaSample/KafkaSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj b/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj index 05cc325a2..35ffd8a2f 100644 --- a/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj +++ b/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj @@ -1,13 +1,12 @@ - 8.0.32.1 Exe net462;net6.0;net8.0 - + diff --git a/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj b/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj index 8078c09ab..627143054 100644 --- a/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj +++ b/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj @@ -1,13 +1,13 @@ - 5.0.7 + 5.0.18 Exe net462;net6.0;net8.0 - + diff --git a/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj b/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj index c4f0d1974..e795de6d9 100644 --- a/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj +++ b/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj @@ -1,14 +1,13 @@ - 3.21.90 Exe net6.0;net8.0 - + diff --git a/test/profiler/applications/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj b/test/profiler/applications/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj index 6c12440f9..f063f5ace 100644 --- a/test/profiler/applications/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj +++ b/test/profiler/applications/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj @@ -1,13 +1,12 @@ - 21.13.0 Exe net462 - + diff --git a/test/profiler/applications/RabbitMqSample/Program.cs b/test/profiler/applications/RabbitMqSample/Program.cs index b43023712..f31c86022 100644 --- a/test/profiler/applications/RabbitMqSample/Program.cs +++ b/test/profiler/applications/RabbitMqSample/Program.cs @@ -96,11 +96,7 @@ private static void PublishAndGet(string name, string exchange, string queue, st Console.WriteLine($"[{name}] BasicPublish - Sent message: {message}"); var result = channel.BasicGet(queue, true); -#if RABBITMQ_6_0 var resultMessage = Encoding.UTF8.GetString(result.Body.ToArray()); -#else - var resultMessage = Encoding.UTF8.GetString(result.Body); -#endif Console.WriteLine($"[{name}] BasicGet - Received message: {resultMessage}"); // ReSharper restore AccessToDisposedClosure }); @@ -135,11 +131,7 @@ private static void PublishAndGetDefault() Console.WriteLine($"[PublishAndGetDefault] BasicPublish - Sent message: {message}"); var result = channel.BasicGet(defaultQueueName, true); -#if RABBITMQ_6_0 var resultMessage = Encoding.UTF8.GetString(result.Body.ToArray()); -#else - var resultMessage = Encoding.UTF8.GetString(result.Body); -#endif Console.WriteLine($"[PublishAndGetDefault] BasicGet - Received message: {resultMessage}"); }); @@ -203,11 +195,7 @@ private static void Receive() var transaction = Agent.Tracer.CurrentTransaction; var span = transaction?.StartSpan("Consume message", ApiConstants.TypeMessaging); -#if RABBITMQ_6_0 var body = ea.Body.ToArray(); -#else - var body = ea.Body; -#endif var message = Encoding.UTF8.GetString(body); Console.WriteLine("[Receive] - [x] Received {0}", message); diff --git a/test/profiler/applications/RabbitMqSample/RabbitMqSample.csproj b/test/profiler/applications/RabbitMqSample/RabbitMqSample.csproj index 83c2d77db..5a824b41a 100644 --- a/test/profiler/applications/RabbitMqSample/RabbitMqSample.csproj +++ b/test/profiler/applications/RabbitMqSample/RabbitMqSample.csproj @@ -1,14 +1,12 @@ - 6.8.1 - $(DefineConstants);RABBITMQ_6_0 Exe net8.0 - + diff --git a/test/profiler/applications/SatelliteAssemblySample/SatelliteAssemblySample.csproj b/test/profiler/applications/SatelliteAssemblySample/SatelliteAssemblySample.csproj index 16fd2a942..8c8215dc6 100644 --- a/test/profiler/applications/SatelliteAssemblySample/SatelliteAssemblySample.csproj +++ b/test/profiler/applications/SatelliteAssemblySample/SatelliteAssemblySample.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/profiler/applications/SqlClientSample/SqlClientSample.csproj b/test/profiler/applications/SqlClientSample/SqlClientSample.csproj index 595cde716..72841a793 100644 --- a/test/profiler/applications/SqlClientSample/SqlClientSample.csproj +++ b/test/profiler/applications/SqlClientSample/SqlClientSample.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/profiler/applications/SqliteSample/SqliteSample.csproj b/test/profiler/applications/SqliteSample/SqliteSample.csproj index 83df7f9d6..1169e0dde 100644 --- a/test/profiler/applications/SqliteSample/SqliteSample.csproj +++ b/test/profiler/applications/SqliteSample/SqliteSample.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs index 21bc9a3da..b75fd8a64 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs @@ -127,6 +127,7 @@ public static DotnetProject Create(string name, string template, string framewor "new", template, "--name", name, "--output", $"\"{directory}\"", + "--no-update-check", "--framework", framework }.Concat(arguments); diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj b/test/startuphook/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj index fd960b3ae..809140a35 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj @@ -5,7 +5,7 @@ - + From c142a4f8ec5dffc1e2905a50a5a12c0ee911e6a1 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 23 Oct 2024 10:20:04 +0200 Subject: [PATCH 43/77] Increase logging of testcontainer (#2468) This also updates TestContainers to a version that's passing the cancellation token to SemaphoreSlim. We now also forcefully break out if disposing the container takes longer then 2 minutes. --- Directory.Packages.props | 16 ++++---- sample/StackExchangeRedisSample/Program.cs | 2 +- .../Azure/AzureCredentialsFactAttribute.cs | 11 ++++++ .../AzureCosmosDbTestEnvironment.cs | 4 ++ .../MicrosoftAzureCosmosTests.cs | 22 +++++------ .../MicrosoftAzureDocumentDbTests.cs | 20 +++++----- .../ElasticsearchFixture.cs | 37 +++++++++++++++++-- .../ElasticsearchTests.cs | 6 ++- .../Fixture/MongoFixture.cs | 31 +++++++++++++++- .../SqlServerFixture.cs | 21 ++++++++++- .../ProfilingSessionTests.cs | 19 +++++++--- .../ElasticsearchTestFixture.cs | 15 ++++++-- .../AdoNet/MySqlFixture.cs | 37 +++++++++++++++++-- .../AdoNet/OracleSqlFixture.cs | 37 +++++++++++++++++-- .../AdoNet/PostgreSqlFixture.cs | 37 +++++++++++++++++-- .../AdoNet/SqlServerFixture.cs | 37 +++++++++++++++++-- .../Kafka/KafkaFixture.cs | 37 +++++++++++++++++-- .../RabbitMq/RabbitMqFixture.cs | 37 +++++++++++++++++-- 18 files changed, 357 insertions(+), 69 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 265aa2053..dc7fefcd7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -65,14 +65,14 @@ - - - - - - - - + + + + + + + + diff --git a/sample/StackExchangeRedisSample/Program.cs b/sample/StackExchangeRedisSample/Program.cs index 1bd03eb4c..28b7029e7 100644 --- a/sample/StackExchangeRedisSample/Program.cs +++ b/sample/StackExchangeRedisSample/Program.cs @@ -44,5 +44,5 @@ await Agent.Tracer.CaptureTransaction("Set and Get String", ApiConstants.TypeDb, await Task.Delay(TimeSpan.FromMilliseconds(100)); } Console.WriteLine("Stopping Redis Container..."); -await container.StopAsync(); +await container.StopAsync(ctx.Token); Console.WriteLine("Exiting"); diff --git a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs index cad1372cc..edb966d6b 100644 --- a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs +++ b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs @@ -18,4 +18,15 @@ public AzureCredentialsFactAttribute() Skip = "Azure credentials not available. If running locally, run `az login` to login"; } } + + public class CosmosDbFactAttribute : FactAttribute + { + public CosmosDbFactAttribute() + { + if (AzureCredentials.Instance is Unauthenticated) + Skip = "Azure credentials not available. If running locally, run `az login` to login"; + if (TestEnvironment.IsCi) + Skip = "CosmosDb tests are temporarily disabled on CI environments"; + } + } } diff --git a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/AzureCosmosDbTestEnvironment.cs b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/AzureCosmosDbTestEnvironment.cs index d973d1672..221607d5b 100644 --- a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/AzureCosmosDbTestEnvironment.cs +++ b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/AzureCosmosDbTestEnvironment.cs @@ -43,6 +43,10 @@ public AzureCosmosDbTestEnvironment(IMessageSink messageSink) if (credentials is Unauthenticated) return; + // TODO remove this and move our cosmosdb tests over to a always ready serverless + if (TestEnvironment.IsCi) + return; + _terraform = new TerraformResources(terraformResourceDirectory, credentials, messageSink); var resourceGroupName = AzureResources.CreateResourceGroupName("cosmosdb-test"); diff --git a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureCosmosTests.cs b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureCosmosTests.cs index bd386f9b9..5383ee671 100644 --- a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureCosmosTests.cs +++ b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureCosmosTests.cs @@ -34,7 +34,7 @@ public MicrosoftAzureCosmosTests(AzureCosmosDbTestEnvironment environment, ITest }); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Database() { await _agent.Tracer.CaptureTransaction("Create CosmosDb Database", ApiConstants.TypeDb, async () => @@ -45,7 +45,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Database", ApiConstants. AssertSpan("Create database"); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Database() { var db = await CreateDatabaseAsync(); @@ -57,7 +57,7 @@ await _agent.Tracer.CaptureTransaction("Delete CosmosDb Database", ApiConstants. AssertSpan($"Delete database {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Get_Database() { var db = await CreateDatabaseAsync(); @@ -69,7 +69,7 @@ await _agent.Tracer.CaptureTransaction("Get CosmosDb Database", ApiConstants.Typ AssertSpan($"Get database {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_List_Databases() { await _agent.Tracer.CaptureTransaction("List CosmosDb Databases", ApiConstants.TypeDb, async () => @@ -83,7 +83,7 @@ await _agent.Tracer.CaptureTransaction("List CosmosDb Databases", ApiConstants.T AssertSpan("List databases"); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Collection() { var db = await CreateDatabaseAsync(); @@ -95,7 +95,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Collection", ApiConstant AssertSpan($"Create collection {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Collection() { var db = await CreateDatabaseAsync(); @@ -111,7 +111,7 @@ await _agent.Tracer.CaptureTransaction("Delete CosmosDb Collection", ApiConstant private string RandomName() => Guid.NewGuid().ToString("N"); - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_List_Collections() { var db = await CreateDatabaseAsync(); @@ -131,7 +131,7 @@ await _agent.Tracer.CaptureTransaction("List CosmosDb Collections", ApiConstants AssertSpan($"List collections {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Document() { var db = await CreateDatabaseAsync(); @@ -147,7 +147,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Item", ApiConstants.Type AssertSpan($"Create/query document {db.Id} {container.Id}", db.Id, 2); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Upsert_Document() { var db = await CreateDatabaseAsync(); @@ -163,7 +163,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Item", ApiConstants.Type AssertSpan($"Create/query document {db.Id} {container.Id}", db.Id, 2); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Document() { var db = await CreateDatabaseAsync(); @@ -181,7 +181,7 @@ await _agent.Tracer.CaptureTransaction("Delete CosmosDb Item", ApiConstants.Type AssertSpan($"Delete document {db.Id} {container.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Replace_Document() { var db = await CreateDatabaseAsync(); diff --git a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureDocumentDbTests.cs b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureDocumentDbTests.cs index cf1c12077..ed759ae82 100644 --- a/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureDocumentDbTests.cs +++ b/test/azure/Elastic.Apm.Azure.CosmosDb.Tests/MicrosoftAzureDocumentDbTests.cs @@ -31,7 +31,7 @@ public MicrosoftAzureDocumentDbTests(AzureCosmosDbTestEnvironment environment, I _client = new DocumentClient(new Uri(environment.Endpoint), environment.PrimaryMasterKey); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Database() { await _agent.Tracer.CaptureTransaction("Create CosmosDb Database", ApiConstants.TypeDb, async () => @@ -42,7 +42,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Database", ApiConstants. AssertSpan("Create database"); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Database() { var db = await CreateDatabaseAsync(); @@ -54,7 +54,7 @@ await _agent.Tracer.CaptureTransaction("Delete CosmosDb Database", ApiConstants. AssertSpan($"Delete database {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Get_Database() { var db = await CreateDatabaseAsync(); @@ -66,7 +66,7 @@ await _agent.Tracer.CaptureTransaction("Get CosmosDb Database", ApiConstants.Typ AssertSpan($"Get database {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_List_Databases() { await _agent.Tracer.CaptureTransaction("List CosmosDb Databases", ApiConstants.TypeDb, async () => @@ -80,7 +80,7 @@ await _agent.Tracer.CaptureTransaction("List CosmosDb Databases", ApiConstants.T AssertSpan("List databases"); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Collection() { var db = await CreateDatabaseAsync(); @@ -92,7 +92,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Collection", ApiConstant AssertSpan($"Create collection {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Collection() { var db = await CreateDatabaseAsync(); @@ -108,7 +108,7 @@ await _agent.Tracer.CaptureTransaction("Delete CosmosDb Collection", ApiConstant private string RandomName() => Guid.NewGuid().ToString("N"); - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_List_Collections() { var db = await CreateDatabaseAsync(); @@ -124,7 +124,7 @@ await _agent.Tracer.CaptureTransaction("List CosmosDb Collections", ApiConstants AssertSpan($"List collections {db.Id}", db.Id); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Create_Document() { var db = await CreateDatabaseAsync(); @@ -140,7 +140,7 @@ await _agent.Tracer.CaptureTransaction("Create CosmosDb Item", ApiConstants.Type AssertSpan($"Create/query document {db.Id} {container.Id}", db.Id, 2); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Delete_Document() { var db = await CreateDatabaseAsync(); @@ -158,7 +158,7 @@ await _client.DeleteDocumentAsync( AssertSpan($"Delete document {db.Id} {container.Id}", db.Id, 2); } - [AzureCredentialsFact] + [CosmosDbFact] public async Task Capture_Span_When_Upsert_Document() { var db = await CreateDatabaseAsync(); diff --git a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs index 33e0bd337..5bfef539a 100644 --- a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs +++ b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs @@ -3,20 +3,49 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Testcontainers.Elasticsearch; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Elasticsearch.Tests { - public sealed class ElasticsearchFixture : IAsyncLifetime + public sealed class ElasticsearchFixture(IMessageSink sink) : IAsyncLifetime { private readonly ElasticsearchContainer _container = new ElasticsearchBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); + } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchFixture)}")); + await _container.DisposeAsync(); } } +} diff --git a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs index b027bb572..be6239a3f 100644 --- a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs +++ b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs @@ -44,7 +44,11 @@ public async Task Elasticsearch_Span_Should_Align_With_Spec() var spans = payloadSender.SpansOnFirstTransaction; - var parentSpan = spans.Where(s => s.ParentId == s.TransactionId).Single(); + // could be 2 because product check generates a span too + var parentSpans = spans.Where(s => s.ParentId == s.TransactionId && s.Action == "request").ToList(); + + parentSpans.Should().HaveCount(1); + var parentSpan = parentSpans[0]; parentSpan.Name.Should().StartWith("Elasticsearch: "); parentSpan.Action.Should().Be("request"); diff --git a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Fixture/MongoFixture.cs b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Fixture/MongoFixture.cs index 4df25cdf5..0c6012f13 100644 --- a/test/instrumentations/Elastic.Apm.MongoDb.Tests/Fixture/MongoFixture.cs +++ b/test/instrumentations/Elastic.Apm.MongoDb.Tests/Fixture/MongoFixture.cs @@ -4,23 +4,32 @@ // Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System; +using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Testcontainers.MongoDb; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.MongoDb.Tests.Fixture { public class MongoFixture : IAsyncLifetime where TConfiguration : IMongoConfiguration, new() { + private readonly IMessageSink _sink; private const string MongoDbImage = "mongo:4.4.5"; private readonly TConfiguration _configuration; private readonly MongoDbContainer _container = new MongoDbBuilder().WithImage(MongoDbImage).Build(); - public MongoFixture() => _configuration = new TConfiguration(); + public MongoFixture(IMessageSink sink) + { + _sink = sink; + _configuration = new TConfiguration(); + } public IMongoCollection Collection { get; private set; } @@ -33,6 +42,11 @@ public async Task InitializeAsync() .GetCollection(_configuration.CollectionName); await _configuration.InitializeAsync(Collection); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + _sink.OnMessage(new DiagnosticMessage(stdOut)); + _sink.OnMessage(new DiagnosticMessage(stdErr)); } public async Task DisposeAsync() @@ -40,7 +54,20 @@ public async Task DisposeAsync() if (Collection != null) await _configuration.DisposeAsync(Collection); - await _container.StopAsync(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(MongoDbContainer)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + _sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(MongoDbContainer)}")); await _container.DisposeAsync(); } } diff --git a/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs b/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs index 1563fe387..3f4a63564 100644 --- a/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs +++ b/test/instrumentations/Elastic.Apm.SqlClient.Tests/SqlServerFixture.cs @@ -3,7 +3,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; @@ -47,6 +49,23 @@ public async Task InitializeAsync() _sink.OnMessage(new DiagnosticMessage(stdErr)); } - public async Task DisposeAsync() => await _container.DisposeAsync(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(SqlServerFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + _sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(SqlServerFixture)}")); + await _container.DisposeAsync(); + } } } diff --git a/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/ProfilingSessionTests.cs b/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/ProfilingSessionTests.cs index 9ace34501..cc5d14a44 100644 --- a/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/ProfilingSessionTests.cs +++ b/test/instrumentations/Elastic.Apm.StackExchange.Redis.Tests/ProfilingSessionTests.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Elastic.Apm.Api; using Elastic.Apm.Tests.Utilities; @@ -21,8 +22,11 @@ public class ProfilingSessionTests [DockerFact] public async Task Capture_Redis_Commands_On_Transaction() { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + await using var container = new RedisBuilder().Build(); - await container.StartAsync(); + await container.StartAsync(cts.Token); var connection = await ConnectionMultiplexer.ConnectAsync(container.GetConnectionString()); var count = 0; @@ -32,7 +36,7 @@ public async Task Capture_Redis_Commands_On_Transaction() if (count < 5) { count++; - await Task.Delay(500); + await Task.Delay(500, cts.Token); } else throw new Exception("Could not connect to redis for integration test"); @@ -73,14 +77,17 @@ await agent.Tracer.CaptureTransaction("Set and Get String", ApiConstants.TypeDb, foreach (var span in payloadSender.Spans) AssertSpan(span); - await container.StopAsync(); + await container.StopAsync(cts.Token); } [DockerFact] public async Task Capture_Redis_Commands_On_Span() { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + await using var container = new RedisBuilder().Build(); - await container.StartAsync(); + await container.StartAsync(cts.Token); var connection = await ConnectionMultiplexer.ConnectAsync(container.GetConnectionString()); var count = 0; @@ -90,7 +97,7 @@ public async Task Capture_Redis_Commands_On_Span() if (count < 5) { count++; - await Task.Delay(500); + await Task.Delay(500, cts.Token); } else throw new Exception("Could not connect to redis for integration test"); @@ -155,7 +162,7 @@ await t.CaptureSpan($"parent span {i}", ApiConstants.TypeDb, async () => AssertSpan(span); } - await container.StopAsync(); + await container.StopAsync(cts.Token); } private static void AssertSpan(ISpan span) diff --git a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs index b3a08035d..fa14b5dff 100644 --- a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs +++ b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs @@ -58,10 +58,19 @@ public async Task InitializeAsync() async Task IAsyncLifetime.DisposeAsync() { - if (Container.State == TestcontainersStates.Running) + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchTestFixture)}")); + await Container.StopAsync(cts.Token); + } + catch (Exception e) { - await Container.StopAsync(); - await Container.DisposeAsync(); + _sink.OnMessage(new DiagnosticMessage(e.Message)); } + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchTestFixture)}")); + await Container.DisposeAsync(); } } diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs index 6e825c69e..3ac93493b 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs @@ -3,23 +3,52 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Testcontainers.MySql; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet { [CollectionDefinition("MySql")] public sealed class MySqlCollection : ICollectionFixture { } - public sealed class MySqlFixture : IAsyncLifetime + public sealed class MySqlFixture(IMessageSink sink) : IAsyncLifetime { private readonly MySqlContainer _container = new MySqlBuilder().WithImage("mysql:8.0.32").Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); + } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(MySqlFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(MySqlFixture)}")); + await _container.DisposeAsync(); } } +} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs index 1a4ccab12..301e6c0dc 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs @@ -3,6 +3,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Elastic.Apm.Tests.Utilities; using Testcontainers.Oracle; @@ -17,12 +19,21 @@ public sealed class OracleCollection : ICollectionFixture { } public sealed class OracleSqlFixture : IAsyncLifetime { - private readonly OracleContainer _container = new OracleBuilder().Build(); + private readonly OracleContainer _container; private readonly IMessageSink _sink; public string ConnectionString => _container.GetConnectionString(); - public OracleSqlFixture(IMessageSink sink) => _sink = sink; + public OracleSqlFixture(IMessageSink sink) + { + _sink = sink; + if (!TestEnvironment.IsWindows) + { + _sink.OnMessage(new DiagnosticMessage($"Skipping {nameof(OracleSqlFixture)} on non windows platforms.")); + return; + } + _container = new OracleBuilder().Build(); + } public async Task InitializeAsync() { @@ -37,6 +48,26 @@ public async Task InitializeAsync() } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + if (!TestEnvironment.IsWindows) + return; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(OracleSqlFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + _sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(OracleSqlFixture)}")); + await _container.DisposeAsync(); + } } } diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs index 472d0eb4a..e57611287 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs @@ -3,23 +3,52 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Testcontainers.PostgreSql; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet { [CollectionDefinition("Postgres")] public sealed class PostgresCollection : ICollectionFixture { } - public sealed class PostgreSqlFixture : IAsyncLifetime + public sealed class PostgreSqlFixture(IMessageSink sink) : IAsyncLifetime { private readonly PostgreSqlContainer _container = new PostgreSqlBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); + } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(PostgreSqlFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(PostgreSqlFixture)}")); + await _container.DisposeAsync(); } } +} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs index 3f25f8dcf..fb802fd04 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs @@ -3,10 +3,14 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet { @@ -15,10 +19,12 @@ public sealed class SqlServerCollection : ICollectionFixture { public sealed class SqlServerFixture : IAsyncLifetime { + private readonly IMessageSink _sink; private readonly MsSqlContainer _container; - public SqlServerFixture() + public SqlServerFixture(IMessageSink sink) { + _sink = sink; // see: https://blog.rufer.be/2024/09/22/workaround-fix-testcontainers-sql-error-docker-dotnet-dockerapiexception-docker-api-responded-with-status-codeconflict/ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -35,8 +41,33 @@ public SqlServerFixture() public string ConnectionString => _container.GetConnectionString(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + _sink.OnMessage(new DiagnosticMessage(stdOut)); + _sink.OnMessage(new DiagnosticMessage(stdErr)); + } + + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(SqlServerFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + _sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(SqlServerFixture)}")); + await _container.DisposeAsync(); + } } } diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs index 80a3bfa4a..f263fc589 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs @@ -3,23 +3,52 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Testcontainers.Kafka; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Profiler.Managed.Tests.Kafka { [CollectionDefinition("Kafka")] public sealed class KafkaCollection : ICollectionFixture { } - public sealed class KafkaFixture : IAsyncLifetime + public sealed class KafkaFixture(IMessageSink sink) : IAsyncLifetime { private readonly KafkaContainer _container = new KafkaBuilder().Build(); - public string BootstrapServers => _container.GetBootstrapAddress(); + public string BootstrapServers => _container.GetBootstrapAddress(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); + } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(KafkaFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(KafkaFixture)}")); + await _container.DisposeAsync(); } } +} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs index 6f6bf2aff..bbf85207c 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs @@ -3,23 +3,52 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; +using System.Threading; using System.Threading.Tasks; using Testcontainers.RabbitMq; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Elastic.Apm.Profiler.Managed.Tests.RabbitMq { [CollectionDefinition("RabbitMq")] public sealed class RabbitMqCollection : ICollectionFixture { } - public sealed class RabbitMqFixture : IAsyncLifetime + public sealed class RabbitMqFixture(IMessageSink sink) : IAsyncLifetime { private readonly RabbitMqContainer _container = new RabbitMqBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + await _container.StartAsync(); + + var (stdOut, stdErr) = await _container.GetLogsAsync(); + + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); + } - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public async Task DisposeAsync() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(RabbitMqFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(RabbitMqFixture)}")); + await _container.DisposeAsync(); } } +} From 582d5000a2f1f2b0cfdafb5794923d7081f0da09 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Wed, 23 Oct 2024 15:30:35 +0200 Subject: [PATCH 44/77] github-actions: use ephemeral github tokens (#2469) --- .github/workflows/release.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc59e7865..929a0f86c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,9 +5,7 @@ on: types: [published] permissions: - contents: write - issues: write - pull-requests: write + contents: read env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages @@ -163,20 +161,37 @@ jobs: needs: [ 'release-windows'] runs-on: ubuntu-latest env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_TAG: v${{ needs.release-windows.outputs.agent-version }} NEW_BRANCH: update/${{ needs.release-windows.outputs.agent-version }} TARGET_BRANCH: ${{ needs.release-windows.outputs.major-version }}.x - steps: + - name: Get token + id: get_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + with: + app_id: ${{ secrets.OBS_AUTOMATION_APP_ID }} + private_key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} + permissions: >- + { + "contents": "write", + "pull_requests": "write" + } + repositories: >- + ["apm-agent-dotnet"] + - uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ steps.get_token.outputs.token }} - name: Setup git config uses: elastic/oblt-actions/git/setup@v1 + with: + github-token: ${{ steps.get_token.outputs.token }} - name: Create GitHub Pull Request if minor release. + env: + GH_TOKEN: ${{ steps.get_token.outputs.token }} run: | echo "as long as there is a major.x branch" existed_in_local=$(git ls-remote --heads origin ${TARGET_BRANCH}) From df7825deeac58c4f2c11808e9143c8eac99a1f0f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 25 Oct 2024 13:57:56 +0200 Subject: [PATCH 45/77] Try to remove netcoreapp2.0 from Elastic.Apm.Profiler.Managed.Loader (#2471) --- build/scripts/Build.fs | 4 ++-- .../Elastic.Apm.Profiler.Managed.Loader.csproj | 5 ++++- src/profiler/elastic_apm_profiler/src/profiler/managed.rs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index 91bd2b372..4962f7ac9 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -277,8 +277,8 @@ module Build = |> Seq.map DirectoryInfo |> Seq.iter (copyDllsAndPdbs (agentDir.CreateSubdirectory(sprintf "%i.0.0" oldDiagnosticSourceVersion.Major))) - // assemblies compiled against 6.0 version of System.Diagnostics.DiagnosticSource - !! (Paths.BuildOutput (sprintf "Elastic.Apm.StartupHook.Loader_%i.0.0/netstandard2.0" oldDiagnosticSourceVersion.Major)) //using old version here, because it the netcoreapp2.2 app can't build with diagnosticsource6 + // assemblies compiled against 6.0 version of System.Diagnostics.DiagnosticSource + !! (Paths.BuildOutput (sprintf "Elastic.Apm.StartupHook.Loader_%i.0.0/netstandard2.0" oldDiagnosticSourceVersion.Major)) ++ (Paths.BuildOutput (sprintf "Elastic.Apm_%i.0.0/net6.0" diagnosticSourceVersion6.Major)) |> Seq.filter Path.isDirectory |> Seq.map DirectoryInfo diff --git a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj index 373ae5b73..03e749485 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj +++ b/src/profiler/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj @@ -2,7 +2,7 @@ Elastic.Apm.Profiler.Managed.Loader - net462;netcoreapp2.0 + net462;netstandard2.0 false false $(DefineConstants);PROFILER_MANAGED_LOADER @@ -15,6 +15,9 @@ + + + diff --git a/src/profiler/elastic_apm_profiler/src/profiler/managed.rs b/src/profiler/elastic_apm_profiler/src/profiler/managed.rs index ff4febb81..2b8b1154e 100644 --- a/src/profiler/elastic_apm_profiler/src/profiler/managed.rs +++ b/src/profiler/elastic_apm_profiler/src/profiler/managed.rs @@ -67,7 +67,7 @@ pub extern "C" fn GetAssemblyAndSymbolsBytes( let tfm = if IS_DESKTOP_CLR.load(Ordering::SeqCst) { "net462" } else { - "netcoreapp2.0" + "netstandard2.0" }; let a = ManagedLoader::get(&format!("{}/{}.dll", tfm, MANAGED_PROFILER_ASSEMBLY_LOADER)).unwrap(); From 0f177cef5af761857d192a6d151137850fbb6c1e Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 8 Nov 2024 11:40:03 +0000 Subject: [PATCH 46/77] Fix span linking for Azure ServiceBus (#2474) Align span ID with activity's span ID in ServiceBus listener The code changes in the `AzureMessagingServiceBusDiagnosticListener.cs` file within the `Elastic.Apm.Azure.ServiceBus` namespace involve modifications to how spans are started for message actions. Specifically, the `StartSpanInternal` method now includes an explicit `id` parameter set to `activity.SpanId.ToString()`. This change ensures that the span ID matches the activity's span ID, which is crucial for correctly linking the consuming span to the producer. This adjustment is necessary because the Azure SDK automatically attaches the `diagnostic-id` and `traceparent` to the message, and proper span linking on the receiver end depends on this alignment. --- .../AzureMessagingServiceBusDiagnosticListener.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/azure/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/azure/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index cc5bed502..3495f04e3 100644 --- a/src/azure/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/azure/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -126,9 +126,13 @@ private void OnMessageStart(KeyValuePair kv, string action) _onMessageCurrent = currentSegment switch { - Span span => span.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant()), + // NOTE: We explicity set the SpanId here to match the Activity (value of the payload) to ensure that span linking on the + // receiver works as expected. The Azure SDK attaches the diagnostic-id and traceparent to the message automatically. + // On the receiving end, we need to be able to correctly link the consuming span to the producer. + + Span span => span.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant(), id: activity.SpanId.ToString()), Transaction transaction => transaction.StartSpanInternal(name, ApiConstants.TypeMessaging, ServiceBus.SubType, - action.ToLowerInvariant()), + action.ToLowerInvariant(), id: activity.SpanId.ToString()), _ => _onMessageCurrent }; } From 57e358e570f5af692f12440cf60a43b17d2b5401 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 12 Nov 2024 11:44:31 +0000 Subject: [PATCH 47/77] Add troubleshooting section for unblocking DLLs (#2481) Add troubleshooting section for unblocking DLLs --- docs/images/unblock-profiler-dll.png | Bin 0 -> 29250 bytes docs/setup-auto-instrumentation.asciidoc | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/images/unblock-profiler-dll.png diff --git a/docs/images/unblock-profiler-dll.png b/docs/images/unblock-profiler-dll.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff52bbe0c5f9f403e67036764c137f052993cf1 GIT binary patch literal 29250 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
nut?=02eY@VEd=r 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 Date: Tue, 12 Nov 2024 12:58:12 +0000 Subject: [PATCH 48/77] Switch to dotnet format rather than dotnet-format tool (#2480) As titled. I will follow up with a PR applying the formatting to all files and then a further PR introducing a check format step in the CI builds. Closes #2327 --- build/scripts/Build.fs | 6 ++---- dotnet-tools.json | 6 ------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index 4962f7ac9..af344d962 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -19,7 +19,7 @@ open Scripts.TestEnvironment open Tooling module Build = - + let private oldDiagnosticSourceVersion = SemVer.parse "4.6.0" let private diagnosticSourceVersion6 = SemVer.parse "6.0.0" @@ -238,9 +238,7 @@ module Build = if isWindows then DotNet.Exec ["restore" ; aspNetFullFramework; "-v"; "q"] let Format () = - ToolRestore() - //dotnet dotnet-format --exclude src/Elastic.Apm/Libraries/ - DotNet.Exec ["dotnet-format"; "--check"; "--exclude"; "src/Elastic.Apm/Libraries/"] + DotNet.Exec ["dotnet"; "format"; "--verbosity"; "quiet"; "--exclude"; "src/Elastic.Apm/Libraries/"] let private copyDllsAndPdbs (destination: DirectoryInfo) (source: DirectoryInfo) = source.GetFiles() diff --git a/dotnet-tools.json b/dotnet-tools.json index 95b93235a..e5fd17baf 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -13,12 +13,6 @@ "commands": [ "minver" ] - }, - "dotnet-format": { - "version": "5.1.250801", - "commands": [ - "dotnet-format" - ] } } } \ No newline at end of file From 1339ad1d84167d042a406825fa6efe9b859c93a0 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 12 Nov 2024 16:28:55 +0000 Subject: [PATCH 49/77] Apply formatting fixes using `dotnet format` (#2483) Reformat all files using `dotnet format` --- .../Elastic.Apm.Benchmarks/Helpers/GitInfo.cs | 6 +- sample/ApiSamples/Program.cs | 4 + .../Controllers/WeatherForecastController.cs | 11 +- src/Elastic.Apm/AgentComponents.cs | 3 +- src/Elastic.Apm/Api/Request.cs | 6 +- .../CentralConfigurationFetcher.cs | 7 +- .../SqlClient/SqlClientDiagnosticListener.cs | 2 +- .../Logging/GlobalLogConfiguration.cs | 126 +++++++++--------- src/Elastic.Apm/Logging/LogValuesFormatter.cs | 3 +- src/Elastic.Apm/Model/SignatureParser.cs | 3 +- .../Reflection/ExpressionBuilder.cs | 12 +- src/Elastic.Apm/Reflection/PropertyFetcher.cs | 4 +- src/Elastic.Apm/ServerInfo/ElasticVersion.cs | 5 +- .../Elastic.Apm.Azure.Storage/BlobUrl.cs | 2 +- .../EfCoreDiagnosticListener.cs | 2 +- .../ServiceCollectionExtensions.cs | 2 +- .../CompositeLogger.cs | 28 ++-- .../Continuations/ContinuationGenerator.cs | 2 + .../DuckTyping/DuckType.Statics.cs | 13 +- .../DuckTyping/LazyILGenerator.cs | 4 +- .../Reflection/MethodBuilder.cs | 5 +- .../Controllers/IntakeV2EventsController.cs | 5 +- .../MetricSetDto.cs | 6 +- .../SolutionPaths.cs | 7 +- .../GlobalLogConfigurationPrecedenceTests.cs | 4 +- .../IisAdministration.cs | 4 +- .../ElasticsearchFixture.cs | 48 +++---- .../ElasticsearchTestFixture.cs | 68 +++++----- .../AdoNet/MySqlFixture.cs | 48 +++---- .../AdoNet/PostgreSqlFixture.cs | 48 +++---- .../DuckTyping/Methods/MethodTests.cs | 14 +- .../DuckTyping/ObscureObject.cs | 2 + .../Kafka/KafkaFixture.cs | 48 +++---- .../RabbitMq/RabbitMqFixture.cs | 48 +++---- .../Elastic.Apm.StartupHook.Sample/Startup.cs | 5 +- .../SolutionPaths.cs | 7 +- 36 files changed, 302 insertions(+), 310 deletions(-) diff --git a/benchmarks/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs b/benchmarks/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs index 665badde5..5f7928cef 100644 --- a/benchmarks/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs +++ b/benchmarks/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs @@ -26,8 +26,10 @@ public GitInfo() WorkingDirectory = Environment.CurrentDirectory }; - _gitProcess = new Process(); - _gitProcess.StartInfo = processInfo; + _gitProcess = new Process + { + StartInfo = processInfo + }; } public string BranchName => RunCommand("rev-parse --abbrev-ref HEAD"); diff --git a/sample/ApiSamples/Program.cs b/sample/ApiSamples/Program.cs index f13d06712..58d3eb5dc 100644 --- a/sample/ApiSamples/Program.cs +++ b/sample/ApiSamples/Program.cs @@ -273,6 +273,7 @@ private static void FilterSample() }); } +#pragma warning disable IDE0022 // ReSharper disable ArrangeMethodOrOperatorBody public static void SampleSpanWithCustomContext() { @@ -291,7 +292,9 @@ public static void SampleSpanWithCustomContextFillAll() transaction.CaptureSpan("SampleSpan1", "SampleSpanType", span => { // ReSharper disable once UseObjectOrCollectionInitializer +#pragma warning disable IDE0017 span.Context.Http = new Http { Url = "http://mysite.com", Method = "GET" }; +#pragma warning restore IDE0017 // send request, get response with status code span.Context.Http.StatusCode = 200; }); @@ -309,6 +312,7 @@ public static void SampleSpanWithCustomContextFillAll() }); } // ReSharper restore ArrangeMethodOrOperatorBody +#pragma warning restore IDE0022 #if NET6_0_OR_GREATER /// diff --git a/sample/WebApiExample/Controllers/WeatherForecastController.cs b/sample/WebApiExample/Controllers/WeatherForecastController.cs index 608694903..1f5ca33e1 100644 --- a/sample/WebApiExample/Controllers/WeatherForecastController.cs +++ b/sample/WebApiExample/Controllers/WeatherForecastController.cs @@ -8,7 +8,16 @@ public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + "Freezing", + "Bracing", + "Chilly", + "Cool", + "Mild", + "Warm", + "Balmy", + "Hot", + "Sweltering", + "Scorching" ]; [HttpGet(Name = "GetWeatherForecast")] diff --git a/src/Elastic.Apm/AgentComponents.cs b/src/Elastic.Apm/AgentComponents.cs index ded9d166f..82898f6e7 100644 --- a/src/Elastic.Apm/AgentComponents.cs +++ b/src/Elastic.Apm/AgentComponents.cs @@ -176,7 +176,7 @@ private void ServerInfoCallback(bool success, IApmServerInfo serverInfo) } #endif } - +#pragma warning disable IDE0022 private static IApmLogger DefaultLogger(IApmLogger logger, IConfigurationReader configurationReader) { #if NETFRAMEWORK @@ -198,6 +198,7 @@ private static IConfigurationReader CreateConfiguration(IApmLogger logger, IConf return configurationReader ?? new EnvironmentConfiguration(configurationLogger); #endif } +#pragma warning restore IDE0022 /// /// This ensures agents will respect externally provided loggers. diff --git a/src/Elastic.Apm/Api/Request.cs b/src/Elastic.Apm/Api/Request.cs index b06e2ef91..c119a069b 100644 --- a/src/Elastic.Apm/Api/Request.cs +++ b/src/Elastic.Apm/Api/Request.cs @@ -31,7 +31,7 @@ public class Request [MaxLength] public string HttpVersion { get; set; } - [MaxLength] [Required] public string Method { get; set; } + [MaxLength][Required] public string Method { get; set; } public Socket Socket { get; set; } @@ -75,9 +75,9 @@ public string Full set => _full = Sanitization.TrySanitizeUrl(value, out var newValue, out _) ? newValue : value; } - [MaxLength] [JsonProperty("hostname")] public string HostName { get; set; } + [MaxLength][JsonProperty("hostname")] public string HostName { get; set; } - [MaxLength] [JsonProperty("pathname")] public string PathName { get; set; } + [MaxLength][JsonProperty("pathname")] public string PathName { get; set; } [MaxLength] public string Protocol { get; set; } diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs index ce9ab8323..6021f6cba 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs @@ -184,14 +184,9 @@ private HttpRequestMessage BuildHttpRequest(EntityTagHeaderValue eTag) _logger.Trace()?.Log("Making HTTP request to APM Server... Request: {HttpRequest}.", httpRequest.RequestUri.Sanitize()); var httpResponse = await HttpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseContentRead, CancellationTokenSource.Token) - .ConfigureAwait(false); - // ReSharper disable once InvertIf - if (httpResponse == null) - { - throw new FailedToFetchConfigException("HTTP client API call for request to APM Server returned null." + .ConfigureAwait(false) ?? throw new FailedToFetchConfigException("HTTP client API call for request to APM Server returned null." + $" Request:{Environment.NewLine}{httpRequest.Sanitize(_configurationStore.CurrentSnapshot.SanitizeFieldNames).ToString().Indent()}", new WaitInfoS(WaitTimeIfAnyError, "HttpResponseMessage from APM Server is null")); - } _logger.Trace()?.Log("Reading HTTP response body... Response: {HttpResponse}.", httpResponse); var httpResponseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); diff --git a/src/Elastic.Apm/Instrumentations/SqlClient/SqlClientDiagnosticListener.cs b/src/Elastic.Apm/Instrumentations/SqlClient/SqlClientDiagnosticListener.cs index 0a90a3eeb..c4f2bc76b 100644 --- a/src/Elastic.Apm/Instrumentations/SqlClient/SqlClientDiagnosticListener.cs +++ b/src/Elastic.Apm/Instrumentations/SqlClient/SqlClientDiagnosticListener.cs @@ -17,7 +17,7 @@ namespace Elastic.Apm.Instrumentations.SqlClient { internal class SqlClientDiagnosticListener : DiagnosticListenerBase { - private ApmAgent _agent; + private readonly ApmAgent _agent; private readonly PropertyFetcherSet _microsoftPropertyFetcherSet = new(); private readonly ConcurrentDictionary _spans = new(); diff --git a/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs b/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs index dfd1ac6b3..d4915ae11 100644 --- a/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs +++ b/src/Elastic.Apm/Logging/GlobalLogConfiguration.cs @@ -27,79 +27,79 @@ internal class EnvironmentLoggingConfiguration(IDictionary environmentVariables { public IDictionary EnvironmentVariables { get; } = environmentVariables ?? Environment.GetEnvironmentVariables(); -public string GetSafeEnvironmentVariable(string key) -{ - var value = EnvironmentVariables.Contains(key) ? EnvironmentVariables[key]?.ToString() : null; - return value ?? string.Empty; -} + public string GetSafeEnvironmentVariable(string key) + { + var value = EnvironmentVariables.Contains(key) ? EnvironmentVariables[key]?.ToString() : null; + return value ?? string.Empty; + } -public LogLevel? GetLogLevel(params string[] keys) -{ - var level = keys - .Select(k => GetSafeEnvironmentVariable(k)) - .Select(v => v.ToLowerInvariant() switch - { - "trace" => LogLevel.Trace, - "debug" => LogLevel.Debug, - "info" => LogLevel.Information, - "warn" => LogLevel.Warning, - "error" => LogLevel.Error, - "none" => LogLevel.None, - _ => null - }) - .FirstOrDefault(l => l != null); - return level; -} + public LogLevel? GetLogLevel(params string[] keys) + { + var level = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .Select(v => v.ToLowerInvariant() switch + { + "trace" => LogLevel.Trace, + "debug" => LogLevel.Debug, + "info" => LogLevel.Information, + "warn" => LogLevel.Warning, + "error" => LogLevel.Error, + "none" => LogLevel.None, + _ => null + }) + .FirstOrDefault(l => l != null); + return level; + } -public string GetLogDirectory(params string[] keys) -{ - var path = keys - .Select(k => GetSafeEnvironmentVariable(k)) - .FirstOrDefault(p => !string.IsNullOrEmpty(p)); + public string GetLogDirectory(params string[] keys) + { + var path = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .FirstOrDefault(p => !string.IsNullOrEmpty(p)); - return path; -} + return path; + } -public bool AnyConfigured(params string[] keys) => - keys - .Select(k => GetSafeEnvironmentVariable(k)) - .Any(p => !string.IsNullOrEmpty(p)); + public bool AnyConfigured(params string[] keys) => + keys + .Select(k => GetSafeEnvironmentVariable(k)) + .Any(p => !string.IsNullOrEmpty(p)); -public GlobalLogTarget? ParseLogTargets(params string[] keys) -{ - var targets = keys - .Select(k => GetSafeEnvironmentVariable(k)) - .FirstOrDefault(p => !string.IsNullOrEmpty(p)); - if (string.IsNullOrWhiteSpace(targets)) - return null; + public GlobalLogTarget? ParseLogTargets(params string[] keys) + { + var targets = keys + .Select(k => GetSafeEnvironmentVariable(k)) + .FirstOrDefault(p => !string.IsNullOrEmpty(p)); + if (string.IsNullOrWhiteSpace(targets)) + return null; - var logTargets = GlobalLogTarget.None; - var found = false; + var logTargets = GlobalLogTarget.None; + var found = false; - foreach (var target in targets.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (IsSet(target, "stdout")) - logTargets |= GlobalLogTarget.StdOut; - else if (IsSet(target, "file")) - logTargets |= GlobalLogTarget.File; - else if (IsSet(target, "none")) - logTargets |= GlobalLogTarget.None; - } - return !found ? null : logTargets; + foreach (var target in targets.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (IsSet(target, "stdout")) + logTargets |= GlobalLogTarget.StdOut; + else if (IsSet(target, "file")) + logTargets |= GlobalLogTarget.File; + else if (IsSet(target, "none")) + logTargets |= GlobalLogTarget.None; + } + return !found ? null : logTargets; - bool IsSet(string k, string v) - { - var b = k.Trim().Equals(v, StringComparison.InvariantCultureIgnoreCase); - if (b) - found = true; - return b; + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, StringComparison.InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } } -} -internal static string GetDefaultLogDirectory() => - Environment.OSVersion.Platform == PlatformID.Win32NT - ? Path.Combine(Environment.GetEnvironmentVariable("PROGRAMDATA")!, "elastic", "apm-agent-dotnet", "logs") - : "/var/log/elastic/apm-agent-dotnet"; + internal static string GetDefaultLogDirectory() => + Environment.OSVersion.Platform == PlatformID.Win32NT + ? Path.Combine(Environment.GetEnvironmentVariable("PROGRAMDATA")!, "elastic", "apm-agent-dotnet", "logs") + : "/var/log/elastic/apm-agent-dotnet"; } [Flags] diff --git a/src/Elastic.Apm/Logging/LogValuesFormatter.cs b/src/Elastic.Apm/Logging/LogValuesFormatter.cs index 2f0486630..ce36cc546 100644 --- a/src/Elastic.Apm/Logging/LogValuesFormatter.cs +++ b/src/Elastic.Apm/Logging/LogValuesFormatter.cs @@ -173,8 +173,7 @@ private object FormatArgument(object value) return value; // if the value implements IEnumerable, build a comma separated string. - var enumerable = value as IEnumerable; - if (enumerable != null) + if (value is IEnumerable enumerable) return string.Join(", ", enumerable.Cast().Select(o => o ?? NullValue)); return value; diff --git a/src/Elastic.Apm/Model/SignatureParser.cs b/src/Elastic.Apm/Model/SignatureParser.cs index 224385fcf..540c6fbf5 100644 --- a/src/Elastic.Apm/Model/SignatureParser.cs +++ b/src/Elastic.Apm/Model/SignatureParser.cs @@ -54,8 +54,7 @@ public void QuerySignature(string query, StringBuilder signature, StringBuilder if (cachedSignature != null) { signature.Append(cachedSignature[0]); - if (dbLink != null) - dbLink.Append(cachedSignature[1]); + dbLink?.Append(cachedSignature[1]); return; } } diff --git a/src/Elastic.Apm/Reflection/ExpressionBuilder.cs b/src/Elastic.Apm/Reflection/ExpressionBuilder.cs index d289cab6b..100920844 100644 --- a/src/Elastic.Apm/Reflection/ExpressionBuilder.cs +++ b/src/Elastic.Apm/Reflection/ExpressionBuilder.cs @@ -23,8 +23,8 @@ public static Func BuildPropertyGetter(s } /// - /// Builds a delegate to get a property from an object. is cast to , - /// with the returned property cast to . + /// Builds a delegate to get a property from an object. is cast to , + /// with the returned property cast to . /// public static Func BuildPropertyGetter(Type type, PropertyInfo propertyInfo) { @@ -36,8 +36,8 @@ public static Func BuildPropertyGetter(Type type, PropertyInfo p } /// - /// Builds a delegate to get a property from an object. is cast to , - /// with the returned property cast to . + /// Builds a delegate to get a property from an object. is cast to , + /// with the returned property cast to . /// public static Func BuildFieldGetter(Type type, FieldInfo fieldInfo) { @@ -49,8 +49,8 @@ public static Func BuildFieldGetter(Type type, FieldInfo fieldIn } /// - /// Builds a delegate to get a property from an object. is cast to , - /// with the returned property cast to . + /// Builds a delegate to get a property from an object. is cast to , + /// with the returned property cast to . /// public static Func BuildPropertyGetter(Type type, string propertyName) { diff --git a/src/Elastic.Apm/Reflection/PropertyFetcher.cs b/src/Elastic.Apm/Reflection/PropertyFetcher.cs index 43b2e2892..ea8a9878b 100644 --- a/src/Elastic.Apm/Reflection/PropertyFetcher.cs +++ b/src/Elastic.Apm/Reflection/PropertyFetcher.cs @@ -26,9 +26,7 @@ public virtual object Fetch(object obj) if (_innerFetcher == null) { var type = obj.GetType().GetTypeInfo(); - var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, PropertyName, StringComparison.OrdinalIgnoreCase)); - if (property == null) - property = type.GetProperty(PropertyName); + var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, PropertyName, StringComparison.OrdinalIgnoreCase)) ?? type.GetProperty(PropertyName); _innerFetcher = PropertyFetch.FetcherForProperty(property); } diff --git a/src/Elastic.Apm/ServerInfo/ElasticVersion.cs b/src/Elastic.Apm/ServerInfo/ElasticVersion.cs index ce3979269..9eea1ade6 100644 --- a/src/Elastic.Apm/ServerInfo/ElasticVersion.cs +++ b/src/Elastic.Apm/ServerInfo/ElasticVersion.cs @@ -178,9 +178,8 @@ private static int CompareComponent(string a, string b, bool lower = false) { var ac = aComps[i]; var bc = bComps[i]; - int anum, bnum; - var isanum = int.TryParse(ac, out anum); - var isbnum = int.TryParse(bc, out bnum); + var isanum = int.TryParse(ac, out var anum); + var isbnum = int.TryParse(bc, out var bnum); int r; if (isanum && isbnum) { diff --git a/src/azure/Elastic.Apm.Azure.Storage/BlobUrl.cs b/src/azure/Elastic.Apm.Azure.Storage/BlobUrl.cs index e1fc940d8..d98348283 100644 --- a/src/azure/Elastic.Apm.Azure.Storage/BlobUrl.cs +++ b/src/azure/Elastic.Apm.Azure.Storage/BlobUrl.cs @@ -28,7 +28,7 @@ public static bool TryCreate(string url, out BlobUrl blobUrl) internal abstract class StorageUrl { - private static char[] SplitDomain = { '.' }; + private static readonly char[] SplitDomain = { '.' }; protected StorageUrl(Uri url) { diff --git a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/EfCoreDiagnosticListener.cs b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/EfCoreDiagnosticListener.cs index 8d45e2dd9..933b41f5d 100644 --- a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/EfCoreDiagnosticListener.cs +++ b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/EfCoreDiagnosticListener.cs @@ -15,7 +15,7 @@ namespace Elastic.Apm.EntityFrameworkCore internal class EfCoreDiagnosticListener : DiagnosticListenerBase { private readonly ConcurrentDictionary _spans = new ConcurrentDictionary(); - private ApmAgent _agent; + private readonly ApmAgent _agent; public EfCoreDiagnosticListener(IApmAgent agent) : base(agent) => _agent = agent as ApmAgent; diff --git a/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs index dbc4734c8..d8d010e43 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs +++ b/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public static IServiceCollection AddElasticApmForAspNetCore(this IServiceCollect { var subs = subscribers.ToList(); subs.Add(new AspNetCoreDiagnosticSubscriber()); - services.AddElasticApm([..subs]); + services.AddElasticApm([.. subs]); } return services; diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs index e7498dc98..efa3b5039 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs @@ -8,28 +8,28 @@ namespace Elastic.Apm.Extensions.Hosting; -internal sealed class CompositeLogger(TraceLogger traceLogger, IApmLogger logger) : IDisposable , IApmLogger +internal sealed class CompositeLogger(TraceLogger traceLogger, IApmLogger logger) : IDisposable, IApmLogger { public TraceLogger TraceLogger { get; } = traceLogger; -public IApmLogger ApmLogger { get; } = logger; + public IApmLogger ApmLogger { get; } = logger; -private bool _isDisposed; + private bool _isDisposed; -public void Dispose() => _isDisposed = true; + public void Dispose() => _isDisposed = true; -public void Log(LogLevel level, TState state, Exception e, Func formatter) -{ - if (_isDisposed) - return; + public void Log(LogLevel level, TState state, Exception e, Func formatter) + { + if (_isDisposed) + return; - if (TraceLogger.IsEnabled(level)) - TraceLogger.Log(level, state, e, formatter); + if (TraceLogger.IsEnabled(level)) + TraceLogger.Log(level, state, e, formatter); - if (ApmLogger.IsEnabled(level)) - ApmLogger.Log(level, state, e, formatter); -} + if (ApmLogger.IsEnabled(level)) + ApmLogger.Log(level, state, e, formatter); + } -public bool IsEnabled(LogLevel logLevel) => ApmLogger.IsEnabled(logLevel) || TraceLogger.IsEnabled(logLevel); + public bool IsEnabled(LogLevel logLevel) => ApmLogger.IsEnabled(logLevel) || TraceLogger.IsEnabled(logLevel); } diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs b/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs index aa0afb710..36957338e 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs @@ -16,6 +16,7 @@ internal class ContinuationGenerator { public virtual TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) => returnValue; +#pragma warning disable IDE0022 [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static TReturn ToTReturn(TFrom returnValue) { @@ -35,5 +36,6 @@ protected static TTo FromTReturn(TReturn returnValue) return ContinuationsHelper.Convert(returnValue); #endif } +#pragma warning restore IDE0022 } } diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs index e7671c080..e4c1ba5c2 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs @@ -57,15 +57,16 @@ public static partial class DuckType [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static long _typeCount; - +#pragma warning disable IDE0044, IDE1006 [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static ConstructorInfo _ignoresAccessChecksToAttributeCtor = - typeof(IgnoresAccessChecksToAttribute).GetConstructor(new Type[] { typeof(string) }); + typeof(IgnoresAccessChecksToAttribute).GetConstructor([typeof(string)]); [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static Dictionary> _ignoresAccessChecksToAssembliesSetDictionary = new Dictionary>(); - +#pragma warning restore IDE0044, IDE1006 + internal static long AssemblyCount => _assemblyCount; internal static long TypeCount => _typeCount; @@ -105,8 +106,10 @@ private static ModuleBuilder GetModuleBuilder(Type targetType, bool isVisible) static ModuleBuilder CreateModuleBuilder(string name, Assembly targetAssembly) { - var assemblyName = new AssemblyName(name + $"_{++_assemblyCount}"); - assemblyName.Version = targetAssembly.GetName().Version; + var assemblyName = new AssemblyName(name + $"_{++_assemblyCount}") + { + Version = targetAssembly.GetName().Version + }; var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); return assemblyBuilder.DefineDynamicModule("MainModule"); } diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs index 627060ab6..d27748b84 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs @@ -17,8 +17,8 @@ namespace Elastic.Apm.Profiler.Managed.DuckTyping { internal class LazyILGenerator { - private ILGenerator _generator; - private List> _instructions; + private readonly ILGenerator _generator; + private readonly List> _instructions; private int _offset; public LazyILGenerator(ILGenerator generator) diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs index 13e43b309..13f6dbe2a 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs @@ -526,10 +526,7 @@ private MethodInfo TryFindMethod() if (methods.Length > 1) throw new ArgumentException($"Unable to safely resolve method, found {methods.Length} matches ({logDetail})"); - var methodInfo = methods.SingleOrDefault(); - - if (methodInfo == null) - throw new ArgumentException($"Unable to resolve method, started with {matchesOnNameAndReturn} by name match ({logDetail})"); + var methodInfo = methods.SingleOrDefault() ?? throw new ArgumentException($"Unable to resolve method, started with {matchesOnNameAndReturn} by name match ({logDetail})"); return methodInfo; } diff --git a/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs b/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs index 74c371e49..23a310ae0 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs @@ -108,10 +108,7 @@ private async Task ParsePayloadLineAndAddToReceivedData(string line) ?.Log("Failed to parse payload line as JSON. Error: {PayloadParsingErrorMessage}, line: `{PayloadLine}'", errorEventArgs.ErrorContext.Error.Message, line); } - }); - - if (payload is null) - throw new ArgumentException("Deserialization failed"); + }) ?? throw new ArgumentException("Deserialization failed"); await HandleParsed(nameof(payload.Error), payload.Error, _mockApmServer.ReceivedData.Errors, _mockApmServer.AddError); await HandleParsed(nameof(payload.Metadata), payload.Metadata, _mockApmServer.ReceivedData.Metadata, _mockApmServer.AddMetadata); diff --git a/test/Elastic.Apm.Tests.MockApmServer/MetricSetDto.cs b/test/Elastic.Apm.Tests.MockApmServer/MetricSetDto.cs index 949138c9a..1133362aa 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/MetricSetDto.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/MetricSetDto.cs @@ -27,8 +27,10 @@ internal class MetricSetDto : ITimestampedDto public override string ToString() { - var resultBuilder = new ToStringBuilder(nameof(MetricSetDto)); - resultBuilder.Add("Timestamp", Timestamp); + var resultBuilder = new ToStringBuilder(nameof(MetricSetDto)) + { + { "Timestamp", Timestamp } + }; var samplesToStringBuilder = new ToStringBuilder(""); foreach (var sample in Samples) resultBuilder.Add(sample.Key, sample.Value); diff --git a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs index bdf94eee8..d8cedbc6e 100644 --- a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs +++ b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs @@ -41,13 +41,8 @@ private static string FindVersionedAgentZip() } var agentZip = Directory.EnumerateFiles(buildOutputDir, "ElasticApmAgent_*.zip", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - - if (agentZip is null) - { - throw new FileNotFoundException($"ElasticApmAgent_*.zip file not found in {buildOutputDir}. " + .FirstOrDefault() ?? throw new FileNotFoundException($"ElasticApmAgent_*.zip file not found in {buildOutputDir}. " + $"Run the build script in the solution root with agent-zip target to build"); - } return agentZip; } diff --git a/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs index 75c0db47d..bce3bd50b 100644 --- a/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs +++ b/test/Elastic.Apm.Tests/Config/GlobalLogConfigurationPrecedenceTests.cs @@ -74,8 +74,8 @@ public void CheckLogTargetsPrecedence() private static GlobalLogConfiguration CreateConfig(params (string key, string v)[] values) { var environment = new Hashtable(); - foreach (var kv in values) - environment.Add(kv.key, kv.v); + foreach (var (key, v) in values) + environment.Add(key, v); var config = GlobalLogConfiguration.FromEnvironment(environment); return config; } diff --git a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs index a45d90d0b..f7ac6928d 100644 --- a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs @@ -424,9 +424,7 @@ private static ConfigurationElementCollection GetEnvVarsConfigCollectionForSampl var config = serverManager.GetApplicationHostConfiguration(); var appPoolsSection = config.GetSection("system.applicationHost/applicationPools"); var appPoolsCollection = appPoolsSection.GetCollection(); - var sampleAppPoolAddElement = FindConfigurationElement(appPoolsCollection, "add", "name", SampleApp.AppPoolName); - if (sampleAppPoolAddElement == null) - throw new InvalidOperationException($"Element for application pool {SampleApp.AppPoolName} not found"); + var sampleAppPoolAddElement = FindConfigurationElement(appPoolsCollection, "add", "name", SampleApp.AppPoolName) ?? throw new InvalidOperationException($"Element for application pool {SampleApp.AppPoolName} not found"); return sampleAppPoolAddElement.GetCollection("environmentVariables"); } diff --git a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs index 5bfef539a..ba204fe5d 100644 --- a/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs +++ b/test/instrumentations/Elastic.Apm.Elasticsearch.Tests/ElasticsearchFixture.cs @@ -17,35 +17,35 @@ public sealed class ElasticsearchFixture(IMessageSink sink) : IAsyncLifetime { private readonly ElasticsearchContainer _container = new ElasticsearchBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public async Task InitializeAsync() - { - await _container.StartAsync(); - - var (stdOut, stdErr) = await _container.GetLogsAsync(); - - sink.OnMessage(new DiagnosticMessage(stdOut)); - sink.OnMessage(new DiagnosticMessage(stdErr)); - } + public async Task InitializeAsync() + { + await _container.StartAsync(); - public async Task DisposeAsync() - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); + var (stdOut, stdErr) = await _container.GetLogsAsync(); - try - { - sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchFixture)}")); - await _container.StopAsync(cts.Token); + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); } - catch (Exception e) + + public async Task DisposeAsync() { - sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchFixture)}")); + await _container.DisposeAsync(); } - - sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchFixture)}")); - await _container.DisposeAsync(); } } -} diff --git a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs index fa14b5dff..5b912ad78 100644 --- a/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs +++ b/test/instrumentations/Elastic.Clients.Elasticsearch.Tests/ElasticsearchTestFixture.cs @@ -17,29 +17,29 @@ public sealed class ElasticsearchTestFixture(IMessageSink sink) : IAsyncLifetime { private readonly IMessageSink _sink = sink; -public ElasticsearchContainer Container { get; } = new ElasticsearchBuilder().Build(); + public ElasticsearchContainer Container { get; } = new ElasticsearchBuilder().Build(); -public ElasticsearchClient? Client { get; private set; } + public ElasticsearchClient? Client { get; private set; } -public async Task InitializeAsync() -{ - await Container.StartAsync(); + public async Task InitializeAsync() + { + await Container.StartAsync(); - var (stdOut, stdErr) = await Container.GetLogsAsync(); + var (stdOut, stdErr) = await Container.GetLogsAsync(); - _sink.OnMessage(new DiagnosticMessage(stdOut)); - _sink.OnMessage(new DiagnosticMessage(stdErr)); + _sink.OnMessage(new DiagnosticMessage(stdOut)); + _sink.OnMessage(new DiagnosticMessage(stdErr)); - var settings = new ElasticsearchClientSettings(new Uri(Container.GetConnectionString())); - settings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); + var settings = new ElasticsearchClientSettings(new Uri(Container.GetConnectionString())); + settings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); - Client = new ElasticsearchClient(settings); - if (Client == null) - throw new Exception("`new ElasticsearchClient(settings)` returned `null`"); + Client = new ElasticsearchClient(settings); + if (Client == null) + throw new Exception("`new ElasticsearchClient(settings)` returned `null`"); - //Increase Elasticsearch high disk watermarks, Github Actions container typically has around - //~7GB free (8%) of the available space. - var response = await Client.Transport.RequestAsync(HttpMethod.PUT, "_cluster/settings", PostData.String(@"{ + //Increase Elasticsearch high disk watermarks, Github Actions container typically has around + //~7GB free (8%) of the available space. + var response = await Client.Transport.RequestAsync(HttpMethod.PUT, "_cluster/settings", PostData.String(@"{ ""persistent"": { ""cluster.routing.allocation.disk.watermark.low"": ""90%"", ""cluster.routing.allocation.disk.watermark.low.max_headroom"": ""100GB"", @@ -52,25 +52,25 @@ public async Task InitializeAsync() } }")); - if (!response.ApiCallDetails.HasSuccessfulStatusCode) - throw new Exception(response.ToString()); -} - -async Task IAsyncLifetime.DisposeAsync() -{ - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); - - try - { - _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchTestFixture)}")); - await Container.StopAsync(cts.Token); + if (!response.ApiCallDetails.HasSuccessfulStatusCode) + throw new Exception(response.ToString()); } - catch (Exception e) + + async Task IAsyncLifetime.DisposeAsync() { - _sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + _sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(ElasticsearchTestFixture)}")); + await Container.StopAsync(cts.Token); + } + catch (Exception e) + { + _sink.OnMessage(new DiagnosticMessage(e.Message)); + } + _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchTestFixture)}")); + await Container.DisposeAsync(); } - _sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(ElasticsearchTestFixture)}")); - await Container.DisposeAsync(); -} } diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs index 3ac93493b..a4269eec8 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs @@ -20,35 +20,35 @@ public sealed class MySqlFixture(IMessageSink sink) : IAsyncLifetime { private readonly MySqlContainer _container = new MySqlBuilder().WithImage("mysql:8.0.32").Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public async Task InitializeAsync() - { - await _container.StartAsync(); - - var (stdOut, stdErr) = await _container.GetLogsAsync(); - - sink.OnMessage(new DiagnosticMessage(stdOut)); - sink.OnMessage(new DiagnosticMessage(stdErr)); - } + public async Task InitializeAsync() + { + await _container.StartAsync(); - public async Task DisposeAsync() - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); + var (stdOut, stdErr) = await _container.GetLogsAsync(); - try - { - sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(MySqlFixture)}")); - await _container.StopAsync(cts.Token); + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); } - catch (Exception e) + + public async Task DisposeAsync() { - sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(MySqlFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(MySqlFixture)}")); + await _container.DisposeAsync(); } - - sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(MySqlFixture)}")); - await _container.DisposeAsync(); } } -} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs index e57611287..6d222eedc 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs @@ -20,35 +20,35 @@ public sealed class PostgreSqlFixture(IMessageSink sink) : IAsyncLifetime { private readonly PostgreSqlContainer _container = new PostgreSqlBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public async Task InitializeAsync() - { - await _container.StartAsync(); - - var (stdOut, stdErr) = await _container.GetLogsAsync(); - - sink.OnMessage(new DiagnosticMessage(stdOut)); - sink.OnMessage(new DiagnosticMessage(stdErr)); - } + public async Task InitializeAsync() + { + await _container.StartAsync(); - public async Task DisposeAsync() - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); + var (stdOut, stdErr) = await _container.GetLogsAsync(); - try - { - sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(PostgreSqlFixture)}")); - await _container.StopAsync(cts.Token); + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); } - catch (Exception e) + + public async Task DisposeAsync() { - sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(PostgreSqlFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(PostgreSqlFixture)}")); + await _container.DisposeAsync(); } - - sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(PostgreSqlFixture)}")); - await _container.DisposeAsync(); } } -} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs index ea51380ad..ddb67dc0a 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs @@ -118,7 +118,7 @@ public void RefParametersMethods(object obscureObject) // Ref object parameter object objValue = 4; // ReSharper disable once SuggestVarOrType_BuiltInTypes - object objValue2 = objValue; + var objValue2 = objValue; duckInterface.GetReferenceObject(ref objValue); duckAbstract.GetReferenceObject(ref objValue); duckVirtual.GetReferenceObject(ref objValue); @@ -172,8 +172,7 @@ public void OutParametersMethods(object obscureObject) var duckVirtual = obscureObject.DuckCast(); // Out parameter - int outValue; - duckInterface.GetOutput(out outValue); + duckInterface.GetOutput(out var outValue); Assert.Equal(42, outValue); duckAbstract.GetOutput(out outValue); Assert.Equal(42, outValue); @@ -181,8 +180,7 @@ public void OutParametersMethods(object obscureObject) Assert.Equal(42, outValue); // Out object parameter - object outObjectValue; - duckInterface.GetOutputObject(out outObjectValue); + duckInterface.GetOutputObject(out var outObjectValue); Assert.Equal(42, (int)outObjectValue); duckAbstract.GetOutputObject(out outObjectValue); Assert.Equal(42, (int)outObjectValue); @@ -190,8 +188,7 @@ public void OutParametersMethods(object obscureObject) Assert.Equal(42, (int)outObjectValue); // Duck type output - IDummyFieldObject outDuckType; - Assert.True(duckInterface.TryGetObscure(out outDuckType)); + Assert.True(duckInterface.TryGetObscure(out var outDuckType)); Assert.NotNull(outDuckType); Assert.Equal(99, outDuckType.MagicNumber); @@ -204,8 +201,7 @@ public void OutParametersMethods(object obscureObject) Assert.Equal(99, outDuckType.MagicNumber); // Object output - object outObject; - Assert.True(duckInterface.TryGetObscureObject(out outObject)); + Assert.True(duckInterface.TryGetObscureObject(out var outObject)); Assert.NotNull(outObject); Assert.Equal(99, outObject.DuckCast().MagicNumber); diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs index a1d76f39b..c7ecb54a0 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs @@ -22,6 +22,7 @@ namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping { public class ObscureObject { +#pragma warning disable IDE0044 private static FieldPublicObject fieldPublicObject = new FieldPublicObject(); private static FieldInternalObject fieldInternalObject = new FieldInternalObject(); private static FieldPrivateObject fieldPrivateObject = new FieldPrivateObject(); @@ -947,3 +948,4 @@ public struct PublicStruct } } } +#pragma warning restore IDE0044 diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs index f263fc589..b6ae91054 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs @@ -20,35 +20,35 @@ public sealed class KafkaFixture(IMessageSink sink) : IAsyncLifetime { private readonly KafkaContainer _container = new KafkaBuilder().Build(); - public string BootstrapServers => _container.GetBootstrapAddress(); + public string BootstrapServers => _container.GetBootstrapAddress(); - public async Task InitializeAsync() - { - await _container.StartAsync(); - - var (stdOut, stdErr) = await _container.GetLogsAsync(); - - sink.OnMessage(new DiagnosticMessage(stdOut)); - sink.OnMessage(new DiagnosticMessage(stdErr)); - } + public async Task InitializeAsync() + { + await _container.StartAsync(); - public async Task DisposeAsync() - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); + var (stdOut, stdErr) = await _container.GetLogsAsync(); - try - { - sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(KafkaFixture)}")); - await _container.StopAsync(cts.Token); + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); } - catch (Exception e) + + public async Task DisposeAsync() { - sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(KafkaFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(KafkaFixture)}")); + await _container.DisposeAsync(); } - - sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(KafkaFixture)}")); - await _container.DisposeAsync(); } } -} diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs index bbf85207c..a60ec599f 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/RabbitMq/RabbitMqFixture.cs @@ -20,35 +20,35 @@ public sealed class RabbitMqFixture(IMessageSink sink) : IAsyncLifetime { private readonly RabbitMqContainer _container = new RabbitMqBuilder().Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString(); - public async Task InitializeAsync() - { - await _container.StartAsync(); - - var (stdOut, stdErr) = await _container.GetLogsAsync(); - - sink.OnMessage(new DiagnosticMessage(stdOut)); - sink.OnMessage(new DiagnosticMessage(stdErr)); - } + public async Task InitializeAsync() + { + await _container.StartAsync(); - public async Task DisposeAsync() - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(2)); + var (stdOut, stdErr) = await _container.GetLogsAsync(); - try - { - sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(RabbitMqFixture)}")); - await _container.StopAsync(cts.Token); + sink.OnMessage(new DiagnosticMessage(stdOut)); + sink.OnMessage(new DiagnosticMessage(stdErr)); } - catch (Exception e) + + public async Task DisposeAsync() { - sink.OnMessage(new DiagnosticMessage(e.Message)); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(2)); + + try + { + sink.OnMessage(new DiagnosticMessage($"Stopping {nameof(RabbitMqFixture)}")); + await _container.StopAsync(cts.Token); + } + catch (Exception e) + { + sink.OnMessage(new DiagnosticMessage(e.Message)); + } + + sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(RabbitMqFixture)}")); + await _container.DisposeAsync(); } - - sink.OnMessage(new DiagnosticMessage($"Disposing {nameof(RabbitMqFixture)}")); - await _container.DisposeAsync(); } } -} diff --git a/test/startuphook/Elastic.Apm.StartupHook.Sample/Startup.cs b/test/startuphook/Elastic.Apm.StartupHook.Sample/Startup.cs index 9518d46aa..251396d9a 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Sample/Startup.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Sample/Startup.cs @@ -12,14 +12,13 @@ public class Startup public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { + public void ConfigureServices(IServiceCollection services) => #if NET5_0_OR_GREATER services.AddControllersWithViews(); #else services.AddMvc(); #endif - } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/SolutionPaths.cs b/test/startuphook/Elastic.Apm.StartupHook.Tests/SolutionPaths.cs index f436cadb4..9be58ce30 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/SolutionPaths.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/SolutionPaths.cs @@ -41,13 +41,8 @@ private static string FindVersionedAgentZip() } var agentZip = Directory.EnumerateFiles(buildOutputDir, "ElasticApmAgent_*.zip", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - - if (agentZip is null) - { - throw new FileNotFoundException($"ElasticApmAgent_*.zip file not found in {buildOutputDir}. " + .FirstOrDefault() ?? throw new FileNotFoundException($"ElasticApmAgent_*.zip file not found in {buildOutputDir}. " + $"Run the build script in the solution root with agent-zip target to build"); - } return agentZip; } From 83d360b46eee493546b822ff367a8f9cdd522630 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 12 Nov 2024 17:04:30 +0000 Subject: [PATCH 50/77] Support K8S_ATTACH environment variable for activation (#2482) Adds support for reading K8S_ATTACH to identify activation method --- src/Elastic.Apm/Api/Service.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Apm/Api/Service.cs b/src/Elastic.Apm/Api/Service.cs index 058640b7a..4a827d2bd 100644 --- a/src/Elastic.Apm/Api/Service.cs +++ b/src/Elastic.Apm/Api/Service.cs @@ -96,7 +96,9 @@ static bool CheckForLoadedAssembly(string name) // Legacy mechanism: if the profiler is loaded add a `p` suffix to Agent.Version service.Agent.Version += "-p"; // Check if profiler was injected via K8S hook. - if (new EnvironmentVariables(logger).SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S")) + var envvars =new EnvironmentVariables(logger); + if (envvars.SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S") + || envvars.SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S_ATTACH")) activationMethod = Consts.ActivationK8SAttach; else activationMethod = Consts.ActivationMethodProfiler; From 94a5216bd40a0d7c2bf9fd4bacc34180ddee490a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:21:19 +0100 Subject: [PATCH 51/77] Bump actions/attest-build-provenance from 1.4.3 to 1.4.4 in the github-actions group (#2479) Bumps the github-actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). --- .github/workflows/release-main.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index f88f36188..b921551bd 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -35,7 +35,7 @@ jobs: run: ./build.sh pack - name: generate build provenance - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/build/output/_packages/*.nupkg" @@ -85,7 +85,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -93,12 +93,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 929a0f86c..ef1f70410 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,7 +78,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -86,12 +86,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" @@ -147,7 +147,7 @@ jobs: run: ./build.bat profiler-zip - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_ZIP_FILE }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_ZIP_FILE }}" From c5abef67b39d03d8bddcb86ea74140f192f7a689 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 19 Nov 2024 09:48:14 +0000 Subject: [PATCH 52/77] Add release notes for 1.30.1 (#2486) As titled. --- CHANGELOG.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a6c943ad0..67d7eaed5 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,14 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.30.1]] +==== 1.30.1 - 2024/11/19 + +===== Bug fixes +{pull}2471[#2471] Remove netcoreapp2.0 from Elastic.Apm.Profiler.Managed.Loader +{pull}2474[#2474] Fix span linking for Azure ServiceBus +{pull}2482[#2482] Support K8S_ATTACH environment variable for activation + [[release-notes-1.30.0]] ==== 1.30.0 - 2024/10/11 From efa86c4b3048eadd5554ff16cec8e0b5eea66538 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 27 Nov 2024 09:12:56 +0000 Subject: [PATCH 53/77] Remove net 6.0 targets (#2498) - Removes unsupported net6.0 targets from libraries and updates build and test projects accordingly. - Updates compiler preprocessor directives. - Updates a few critical dependencies to ensure the build and CI processes continue functioning. - Refactors StartupHookTests to ensure all dotnet commands run on 8.0.404 SDK. This can be updated and automated as part of upgrading to the .NET 9 SDK in a future PR. Additional logging is added when debugging test failures locally. Closes #2489 --- .github/workflows/bootstrap/action.yml | 2 +- .../workflows/install-dependencies/action.yml | 6 +- .github/workflows/test-linux.yml | 12 +- .github/workflows/test-windows.yml | 12 +- Directory.Packages.props | 44 +- ElasticApmAgent.sln | 8 +- build/scripts/Build.fs | 26 +- sample/ApiSamples/Program.cs | 2 +- src/Elastic.Apm/Api/Service.cs | 4 +- src/Elastic.Apm/Elastic.Apm.csproj | 4 +- .../Filters/CookieHeaderRedactionFilter.cs | 6 +- .../Newtonsoft.Json/JsonException.cs | 8 +- .../Serialization/JsonFormatterConverter.cs | 10 +- .../Serialization/JsonObjectContract.cs | 8 +- .../JsonSerializerInternalReader.cs | 6 +- .../JsonSerializerInternalWriter.cs | 6 +- .../Newtonsoft.Json/Utilities/AsyncUtils.cs | 2 +- .../Logging/IApmLoggingExtensions.cs | 2 +- src/Elastic.Apm/Logging/ScopedLogger.cs | 2 +- .../Metrics/Linux/GlobalMemoryStatus.cs | 6 +- .../MetricsProvider/CgroupMetricsProvider.cs | 10 +- .../OpenTelemetry/ElasticActivityListener.cs | 4 +- src/Elastic.Apm/Report/PayloadSenderV2.cs | 2 +- .../Elastic.Apm.Azure.ServiceBus.csproj | 2 +- .../Elastic.Apm.EntityFrameworkCore.csproj | 4 +- .../Elastic.Apm.MongoDb/LICENSE | 416 +++++++++--------- .../ApplicationBuilderExtensions.cs | 2 +- .../Elastic.Apm.AspNetCore.csproj | 4 +- .../CompositeLogger.cs | 2 - .../Elastic.Apm.Extensions.Hosting.csproj | 30 +- .../ServiceCollectionExtensions.cs | 2 +- .../Elastic.Apm.Extensions.Logging.csproj | 6 +- .../Elastic.Apm.NetCoreAll.csproj | 2 +- .../Elastic.Apm.Profiler.Managed.Core.csproj | 2 +- .../DuckTyping/DuckType.Statics.cs | 2 +- test/Elastic.Apm.Tests/LoggerTests.cs | 2 +- .../AzureFunctionsInProcessTests.cs | 10 +- .../Elastic.Apm.AzureFunctionApp.Core.csproj | 2 +- .../Elastic.AzureFunctionApp.InProcess.csproj | 2 +- .../local.settings.json | 3 +- .../AspNetCoreBasicTests.cs | 4 +- .../SampleAspNetCoreApp.csproj | 10 +- .../SampleConsoleNetCoreApp.csproj | 2 +- .../WebApiSample/WebApiSample.csproj | 2 +- .../AdoNet/AdoNetTestData.cs | 1 - .../ValueTaskContinuationGeneratorTests.cs | 4 +- .../DuckTyping/DuckIgnoreTests.cs | 2 +- .../Elastic.Apm.AdoNet.csproj | 2 +- .../MySqlDataSample/MySqlDataSample.csproj | 2 +- .../NpgsqlSample/NpgsqlSample.csproj | 2 +- .../OracleManagedDataAccessCoreSample.csproj | 2 +- .../SqlClientSample/SqlClientSample.csproj | 2 +- .../SqliteSample/SqliteSample.csproj | 2 +- .../Elastic.Apm.StartupHook.Sample.csproj | 2 +- .../DotnetProject.cs | 102 ++++- .../StartupHookTests.cs | 138 +++--- 56 files changed, 534 insertions(+), 430 deletions(-) diff --git a/.github/workflows/bootstrap/action.yml b/.github/workflows/bootstrap/action.yml index 6dd6532b3..d5b4b52d5 100644 --- a/.github/workflows/bootstrap/action.yml +++ b/.github/workflows/bootstrap/action.yml @@ -45,13 +45,13 @@ runs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 6.0.x 8.0.x - id: dotnet shell: bash run: | dotnet --list-sdks + dotnet --version dotnet tool restore AGENT_VERSION=$(dotnet minver -t=v -p=canary.0 -v=e) echo "Version Number: ${AGENT_VERSION}" diff --git a/.github/workflows/install-dependencies/action.yml b/.github/workflows/install-dependencies/action.yml index df05f3bde..0b4f65c99 100644 --- a/.github/workflows/install-dependencies/action.yml +++ b/.github/workflows/install-dependencies/action.yml @@ -59,15 +59,15 @@ if: "${{ inputs.azure == 'true' && runner.os == 'Linux' }}" shell: bash run: | - wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb + wget -q https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb sudo apt-get update - sudo apt-get install azure-functions-core-tools-4 + sudo apt-get install azure-functions-core-tools-4=4.0.6280-1 - name: 'Windows: Azure functions core tools' if: "${{ inputs.azure == 'true' && runner.os == 'Windows' }}" shell: cmd - run: choco install azure-functions-core-tools -y --no-progress -r --version 4.0.4829 + run: choco install azure-functions-core-tools -y --no-progress -r --version 4.0.6280 # TEST CONTAINERS CLOUD # If no PR event or if a PR event that's caused by a non-fork and non dependabot actor diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index c44a0772d..338bad2f9 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -123,7 +123,15 @@ jobs: - name: 'Tests: Integrations' run: ./build.sh test --test-suite integrations - + + - name: Store crash dumps + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: results + retention-days: 1 + path: build/output/**/*.dmp + startup-hook-tests: runs-on: ubuntu-latest needs: [ 'format', 'tests' ] @@ -134,7 +142,7 @@ jobs: - name: 'Tests: StartupHooks' run: ./build.sh test --test-suite startuphooks - + profiler-tests: runs-on: ubuntu-latest needs: [ 'format', 'tests' ] diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 610314bf7..96ac00e54 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -71,6 +71,14 @@ jobs: - name: 'Tests: Integrations' run: ./build.bat test --test-suite integrations + + - name: Store crash dumps + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: results + retention-days: 1 + path: build/output/**/*.dmp startup-hook-tests: runs-on: windows-2022 @@ -82,7 +90,7 @@ jobs: - name: 'Tests: StartupHooks' run: ./build.bat test --test-suite startuphooks - + profiler-tests: runs-on: windows-2022 needs: [ 'format', 'tests' ] @@ -114,7 +122,6 @@ jobs: - name: Add msbuild to PATH uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2 - - name: Clean the application run: msbuild /t:Clean /p:Configuration=Release @@ -166,7 +173,6 @@ jobs: --logger:"junit;LogFilePath=%cd%\build\output\junit-{framework}-{assembly}.xml;MethodFormat=Class;FailureBodyFormat=Verbose" - name: Store test results - if: success() || failure() uses: actions/upload-artifact@v4 with: name: test-results-iis diff --git a/Directory.Packages.props b/Directory.Packages.props index dc7fefcd7..8e37e4d0c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,10 +2,9 @@ true - - + @@ -53,15 +52,15 @@ - + - + - + - - + + @@ -77,7 +76,6 @@ - @@ -98,28 +96,26 @@ - + - - + + - + - - - - - - - - - - + + + + + + + + @@ -127,14 +123,14 @@ - + - + diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index eb3268681..56ed373a1 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -18,9 +18,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAspNetCoreApp", "test EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{267A241E-571F-458F-B04C-B6C4DE79E735}" ProjectSection(SolutionItems) = preProject + test\.runsettings = test\.runsettings test\Directory.Build.props = test\Directory.Build.props test\xunit.runner.json = test\xunit.runner.json - test\.runsettings = test\.runsettings EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm", "src\Elastic.Apm\Elastic.Apm.csproj", "{90BC9629-C8D2-4FD5-863E-EA2D5FB37341}" @@ -240,6 +240,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostingTestApp", "test\inte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Extensions.Tests.Shared", "test\integrations\Elastic.Apm.Extensions.Tests.Shared\Elastic.Apm.Extensions.Tests.Shared.csproj", "{7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A07D777-DEC2-4BB7-B4A1-028BA658762A}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index af344d962..4baf887d8 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -173,13 +173,27 @@ module Build = let testElseWhere = ["Tests"; "OpenTelemetry.Tests"; "StartupHook.Tests"; "Profiler.Managed.Tests"; "Azure"] let filter = testElseWhere |> List.map (fun s -> $"FullyQualifiedName!~Elastic.Apm.%s{s}") |> String.concat "&" Some filter - | _ -> None + | _ -> None + + let verbosity = + match suite with + | TestSuite.Integrations -> "detailed" + | _ -> "minimal" + + let blame = + match suite with + | TestSuite.Integrations -> Some [ + "--blame-hang-timeout"; "15m"; + "--blame-crash-dump-type"; "mini"; + "--results-directory"; "build/output"] + | _ -> None let command = - ["test"; "-c"; "Release"; sln; "--no-build"; "--verbosity"; "minimal"; "-s"; "test/.runsettings"] + ["test"; "-c"; "Release"; sln; "--no-build"; "--verbosity"; verbosity; "-s"; "test/.runsettings"] @ (match filter with None -> [] | Some f -> ["--filter"; f]) @ (match framework with None -> [] | Some f -> ["-f"; f]) @ (match logger with None -> [] | Some l -> [l]) + @ (match blame with None -> [] | Some l -> l) @ ["--"; "RunConfiguration.CollectSourceInformation=true"] DotNet.ExecWithTimeout command (TimeSpan.FromMinutes 30) @@ -236,16 +250,14 @@ module Build = ToolRestore() DotNet.Exec ["restore" ; Paths.Solution; "-v"; "q"] if isWindows then DotNet.Exec ["restore" ; aspNetFullFramework; "-v"; "q"] - let Format () = - DotNet.Exec ["dotnet"; "format"; "--verbosity"; "quiet"; "--exclude"; "src/Elastic.Apm/Libraries/"] - + DotNet.Exec ["format"; "--verbosity"; "quiet"; "--verify-no-changes"; "--exclude"; "src/Elastic.Apm/Libraries/"] let private copyDllsAndPdbs (destination: DirectoryInfo) (source: DirectoryInfo) = source.GetFiles() |> Seq.filter (fun file -> file.Extension = ".dll" || file.Extension = ".pdb") |> Seq.iter (fun file -> file.CopyTo(Path.combine destination.FullName file.Name, true) |> ignore) - /// Creates versioned ElasticApmAgent.zip file + /// Creates versioned ElasticApmAgent.zip file let AgentZip () = let name = "ElasticApmAgent" let currentAssemblyVersion = Versioning.CurrentVersion.FileVersion @@ -277,7 +289,7 @@ module Build = // assemblies compiled against 6.0 version of System.Diagnostics.DiagnosticSource !! (Paths.BuildOutput (sprintf "Elastic.Apm.StartupHook.Loader_%i.0.0/netstandard2.0" oldDiagnosticSourceVersion.Major)) - ++ (Paths.BuildOutput (sprintf "Elastic.Apm_%i.0.0/net6.0" diagnosticSourceVersion6.Major)) + ++ (Paths.BuildOutput (sprintf "Elastic.Apm_%i.0.0/net8.0" diagnosticSourceVersion6.Major)) |> Seq.filter Path.isDirectory |> Seq.map DirectoryInfo |> Seq.iter (copyDllsAndPdbs (agentDir.CreateSubdirectory(sprintf "%i.0.0" diagnosticSourceVersion6.Major))) diff --git a/sample/ApiSamples/Program.cs b/sample/ApiSamples/Program.cs index 58d3eb5dc..ef51c3f08 100644 --- a/sample/ApiSamples/Program.cs +++ b/sample/ApiSamples/Program.cs @@ -314,7 +314,7 @@ public static void SampleSpanWithCustomContextFillAll() // ReSharper restore ArrangeMethodOrOperatorBody #pragma warning restore IDE0022 -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER /// /// Test for https://github.com/elastic/apm-agent-dotnet/issues/884 /// diff --git a/src/Elastic.Apm/Api/Service.cs b/src/Elastic.Apm/Api/Service.cs index 4a827d2bd..07107c7a9 100644 --- a/src/Elastic.Apm/Api/Service.cs +++ b/src/Elastic.Apm/Api/Service.cs @@ -96,8 +96,8 @@ static bool CheckForLoadedAssembly(string name) // Legacy mechanism: if the profiler is loaded add a `p` suffix to Agent.Version service.Agent.Version += "-p"; // Check if profiler was injected via K8S hook. - var envvars =new EnvironmentVariables(logger); - if (envvars.SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S") + var envvars = new EnvironmentVariables(logger); + if (envvars.SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S") || envvars.SafeCheckValue("ELASTIC_APM_ACTIVATION_METHOD", "K8S_ATTACH")) activationMethod = Consts.ActivationK8SAttach; else diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index ef7b4ae93..361acc507 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net462;net472;net6.0 + netstandard2.0;net462;net472;net8.0 true @@ -103,7 +103,7 @@ - + diff --git a/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs b/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs index ed81382fb..b8574c057 100644 --- a/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs +++ b/src/Elastic.Apm/Filters/CookieHeaderRedactionFilter.cs @@ -9,7 +9,7 @@ using Elastic.Apm.Config; using Elastic.Apm.Helpers; using Elastic.Apm.Model; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER using System.Buffers; #endif @@ -45,7 +45,7 @@ internal static void HandleCookieHeader(Dictionary headers, IRea // e.g. Cookies | cookies | COOKIES const int maxKeys = 4; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER var matchedKeys = ArrayPool.Shared.Rent(maxKeys); var matchedValues = ArrayPool.Shared.Rent(maxKeys); #else @@ -81,7 +81,7 @@ internal static void HandleCookieHeader(Dictionary headers, IRea } } -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER ArrayPool.Shared.Return(matchedKeys); ArrayPool.Shared.Return(matchedValues); #endif diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/JsonException.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/JsonException.cs index a570b8ab7..c0548f1c2 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/JsonException.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/JsonException.cs @@ -73,9 +73,11 @@ public JsonException(string message, Exception? innerException) /// The parameter is null. /// The class name is null or is zero (0). public JsonException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } +#pragma warning disable SYSLIB0051 + : base(info, context) +#pragma warning restore SYSLIB0051 + { + } #endif internal static JsonException Create(IJsonLineInfo lineInfo, string path, string message) diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs index bc8c03805..1282cd9cf 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs @@ -1,4 +1,4 @@ -#region License +#region License // Copyright (c) 2007 James Newton-King // @@ -35,9 +35,11 @@ #nullable enable namespace Elastic.Apm.Libraries.Newtonsoft.Json.Serialization { - internal class JsonFormatterConverter : IFormatterConverter - { - private readonly JsonSerializerInternalReader _reader; +#pragma warning disable SYSLIB0050 + internal class JsonFormatterConverter : IFormatterConverter +#pragma warning restore SYSLIB0050 + { + private readonly JsonSerializerInternalReader _reader; private readonly JsonISerializableContract _contract; private readonly JsonProperty? _member; diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonObjectContract.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonObjectContract.cs index f23fe0274..47dd88017 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonObjectContract.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonObjectContract.cs @@ -1,4 +1,4 @@ -#region License +#region License // Copyright (c) 2007 James Newton-King // @@ -183,8 +183,10 @@ internal object GetUninitializedObject() throw new JsonException("Insufficient permissions. Creating an uninitialized '{0}' type requires full trust.".FormatWith(CultureInfo.InvariantCulture, NonNullableUnderlyingType)); } - return FormatterServices.GetUninitializedObject(NonNullableUnderlyingType); - } +#pragma warning disable SYSLIB0050 + return FormatterServices.GetUninitializedObject(NonNullableUnderlyingType); +#pragma warning restore SYSLIB0050 + } #endif } } diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs index 92ea3d3b0..ade850b08 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs @@ -1552,9 +1552,11 @@ private object CreateISerializable(JsonReader reader, JsonISerializableContract TraceWriter.Trace(TraceLevel.Info, JsonPosition.FormatMessage(reader as IJsonLineInfo, reader.Path, "Deserializing {0} using ISerializable constructor.".FormatWith(CultureInfo.InvariantCulture, contract.UnderlyingType)), null); } - SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, new JsonFormatterConverter(this, contract, member)); +#pragma warning disable SYSLIB0050 + SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, new JsonFormatterConverter(this, contract, member)); +#pragma warning restore SYSLIB0050 - bool finished = false; + bool finished = false; do { switch (reader.TokenType) diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs index f79534f72..2bc1540b8 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs @@ -761,10 +761,12 @@ private void SerializeISerializable(JsonWriter writer, ISerializable value, Json WriteObjectStart(writer, value, contract, member, collectionContract, containerProperty); - SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, new FormatterConverter()); +#pragma warning disable SYSLIB0050 + SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, new FormatterConverter()); value.GetObjectData(serializationInfo, Serializer._context); +#pragma warning restore SYSLIB0050 - foreach (SerializationEntry serializationEntry in serializationInfo) + foreach (SerializationEntry serializationEntry in serializationInfo) { JsonContract? valueContract = GetContractSafe(serializationEntry.Value); diff --git a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Utilities/AsyncUtils.cs b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Utilities/AsyncUtils.cs index 063bf03da..7fae325f5 100644 --- a/src/Elastic.Apm/Libraries/Newtonsoft.Json/Utilities/AsyncUtils.cs +++ b/src/Elastic.Apm/Libraries/Newtonsoft.Json/Utilities/AsyncUtils.cs @@ -100,7 +100,7 @@ public static Task ReadAsync(this TextReader reader, char[] buffer, int ind public static bool IsCompletedSucessfully(this Task task) { // IsCompletedSuccessfully is the faster method, but only currently exposed on .NET Core 2.0 -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER return task.IsCompletedSuccessfully; #else return task.Status == TaskStatus.RanToCompletion; diff --git a/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs b/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs index b645b54ca..4b9b4521c 100644 --- a/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs +++ b/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs @@ -72,7 +72,7 @@ private static LogValuesFormatter GetOrAddFormatter(string message, IReadOnlyCol return formatter; formatter = new LogValuesFormatter(message, args); -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER Formatters.AddOrUpdate(message, formatter); return formatter; #else diff --git a/src/Elastic.Apm/Logging/ScopedLogger.cs b/src/Elastic.Apm/Logging/ScopedLogger.cs index fd01dd417..ed49289f1 100644 --- a/src/Elastic.Apm/Logging/ScopedLogger.cs +++ b/src/Elastic.Apm/Logging/ScopedLogger.cs @@ -30,7 +30,7 @@ internal LogValuesFormatter GetOrAddFormatter(string message, IReadOnlyCollectio return formatter; formatter = new LogValuesFormatter($"{{{{{{Scope}}}}}} {message}", args, Scope); -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER Formatters.AddOrUpdate(message, formatter); return formatter; #else diff --git a/src/Elastic.Apm/Metrics/Linux/GlobalMemoryStatus.cs b/src/Elastic.Apm/Metrics/Linux/GlobalMemoryStatus.cs index 0cb69e84a..025a097c6 100644 --- a/src/Elastic.Apm/Metrics/Linux/GlobalMemoryStatus.cs +++ b/src/Elastic.Apm/Metrics/Linux/GlobalMemoryStatus.cs @@ -8,7 +8,7 @@ using System.Runtime.InteropServices; using Elastic.Apm.Logging; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER using System.Buffers; using System.Buffers.Text; #endif @@ -19,7 +19,7 @@ internal static class GlobalMemoryStatus { public const string ProcMemInfo = "/proc/meminfo"; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER private static readonly FileStreamOptions Options = new() { BufferSize = 0, Mode = FileMode.Open, Access = FileAccess.Read }; private static readonly byte Space = (byte)' '; @@ -57,7 +57,7 @@ internal static (long totalMemory, long availableMemory) GetTotalAndAvailableSys } try { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER using var fs = new FileStream(memInfoPath, Options); var buffer = ArrayPool.Shared.Rent(8192); // Should easily be large enough for max meminfo file. diff --git a/src/Elastic.Apm/Metrics/MetricsProvider/CgroupMetricsProvider.cs b/src/Elastic.Apm/Metrics/MetricsProvider/CgroupMetricsProvider.cs index fad8b88d1..d1f64a488 100644 --- a/src/Elastic.Apm/Metrics/MetricsProvider/CgroupMetricsProvider.cs +++ b/src/Elastic.Apm/Metrics/MetricsProvider/CgroupMetricsProvider.cs @@ -12,7 +12,7 @@ using Elastic.Apm.Helpers; using Elastic.Apm.Logging; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER using System.Buffers.Text; using System.Runtime.CompilerServices; using System.Text; @@ -46,7 +46,7 @@ internal class CgroupMetricsProvider : IMetricsProvider internal static readonly Regex Cgroup2MountPoint = new("^\\d+? \\d+? .+? .+? (.*?) .*cgroup2.*cgroup.*"); internal static readonly Regex MemoryCgroup = new("^\\d+:memory:.*"); -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER private static readonly FileStreamOptions Options = new() { BufferSize = 0, Mode = FileMode.Open, Access = FileAccess.Read }; #endif @@ -316,7 +316,7 @@ private MetricSample GetMemoryMemLimitBytes() return new MetricSample(SystemProcessCgroupMemoryMemLimitBytes, totalMemory); } -#if NET6_0_OR_GREATER // Optimised code for newer runtimes +#if NET8_0_OR_GREATER // Optimised code for newer runtimes return GetLongValueFromFile(_cGroupFiles.MaxMemoryFile, SystemProcessCgroupMemoryMemLimitBytes); #else using var reader = new StreamReader(_cGroupFiles.MaxMemoryFile); @@ -339,7 +339,7 @@ private MetricSample GetMemoryMemUsageBytes() { try { -#if NET6_0_OR_GREATER // Optimised code for newer runtimes +#if NET8_0_OR_GREATER // Optimised code for newer runtimes return GetLongValueFromFile(_cGroupFiles.UsedMemoryFile, SystemProcessCgroupMemoryMemUsageBytes); #else using var reader = new StreamReader(_cGroupFiles.UsedMemoryFile); @@ -356,7 +356,7 @@ private MetricSample GetMemoryMemUsageBytes() return null; } -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining)] private MetricSample GetLongValueFromFile(string path, string sampleName) { diff --git a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs index d045e8ceb..dca9db9c7 100644 --- a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs +++ b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs @@ -200,7 +200,7 @@ private void CreateSpanForActivity(Activity activity, long timestamp, List(response.Content.ReadAsStream()); #else var intakeResponse = _payloadItemSerializer.Deserialize(response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()); diff --git a/src/azure/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj b/src/azure/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj index 391eebd8a..f703a4abc 100644 --- a/src/azure/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj +++ b/src/azure/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj @@ -1,7 +1,7 @@ - netstandard2.0; net6.0 + netstandard2.0;net8.0 Elastic.Apm.Azure.ServiceBus Elastic.Apm.Azure.ServiceBus Elastic.Apm.Azure.ServiceBus diff --git a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj index cbedad1a4..46bf44606 100644 --- a/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj +++ b/src/instrumentations/Elastic.Apm.EntityFrameworkCore/Elastic.Apm.EntityFrameworkCore.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net6.0 + netstandard2.0;net8.0 Elastic.Apm.EntityFrameworkCore Elastic.Apm.EntityFrameworkCore Elastic.Apm.EntityFrameworkCore @@ -11,7 +11,7 @@ - + diff --git a/src/instrumentations/Elastic.Apm.MongoDb/LICENSE b/src/instrumentations/Elastic.Apm.MongoDb/LICENSE index ad5f27ac6..950d224c1 100644 --- a/src/instrumentations/Elastic.Apm.MongoDb/LICENSE +++ b/src/instrumentations/Elastic.Apm.MongoDb/LICENSE @@ -1,208 +1,208 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Elasticsearch BV - - Licensed 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. - -========== -Elastic.Apm.MongoDb ----------- - -The code for Elastic.Apm.MongoDb is based on the elastic-apm-mongo project by Vadim Hatsura (@vhatsura), -licensed under the Apache 2.0 License. https://github.com/vhatsura/elastic-apm-mongo +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Elasticsearch BV + + Licensed 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. + +========== +Elastic.Apm.MongoDb +---------- + +The code for Elastic.Apm.MongoDb is based on the elastic-apm-mongo project by Vadim Hatsura (@vhatsura), +licensed under the Apache 2.0 License. https://github.com/vhatsura/elastic-apm-mongo diff --git a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs index 1efbfe841..3cc8e7465 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs @@ -93,7 +93,7 @@ params IDiagnosticsSubscriber[] subscribers } private static string GetEnvironmentName(this IServiceProvider serviceProvider) => -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER (serviceProvider.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment)?.EnvironmentName; #else #pragma warning disable CS0246 diff --git a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj index 245a1fe50..54566d7eb 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj +++ b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj @@ -5,7 +5,7 @@ Elastic.Apm.AspNetCore Elastic APM for ASP.NET Core. This package contains auto instrumentation for ASP.NET Core. See: https://github.com/elastic/apm-agent-dotnet/tree/main/docs apm, monitoring, elastic, elasticapm, analytics, aspnetcore - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;netstandard2.1;net8.0 true @@ -23,7 +23,7 @@ - + diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs index efa3b5039..6c3bddee8 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/CompositeLogger.cs @@ -30,6 +30,4 @@ public void Log(LogLevel level, TState state, Exception e, Func ApmLogger.IsEnabled(logLevel) || TraceLogger.IsEnabled(logLevel); - - } diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj index 96cd9a6e0..0a148d83f 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj @@ -4,22 +4,20 @@ Elastic.Apm.Extensions.Hosting Elastic APM .NET Agent. This package offers integration with Microsoft.Extensions.Hosting.IHostBuilder for agent registration apm, monitoring, elastic, elasticapm, analytics, netcore - netstandard2.0;net6.0 + netstandard2.0;net8.0 true - - - - - - - - - + + + + + + + @@ -29,11 +27,13 @@ - - - - - + + + + + + + diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs index 2931fe1a3..afb85db01 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs @@ -106,7 +106,7 @@ public static IServiceCollection AddElasticApm(this IServiceCollection services, } private static string GetDefaultEnvironmentName(IServiceProvider serviceProvider) => -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER (serviceProvider.GetService(typeof(IHostEnvironment)) as IHostEnvironment)?.EnvironmentName; // This is preferred since 3.0 #else #pragma warning disable CS0246 diff --git a/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj b/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj index 88cd762a6..0c9255e35 100644 --- a/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Logging/Elastic.Apm.Extensions.Logging.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0 + netstandard2.0;net8.0 Elastic.Apm.Extensions.Logging Elastic.Apm.Extensions.Logging Elastic.Apm.Extensions.Logging @@ -19,8 +19,8 @@ - - + + diff --git a/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj b/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj index 1d1ba10d2..583242880 100644 --- a/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj +++ b/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net6.0 + netstandard2.0;net8.0 Elastic.Apm.NetCoreAll Elastic.Apm.NetCoreAll Elastic APM .NET agent. This is a convenient package that automatically pulls in ASP.NET Core, and Entity Framework Core auto instrumentation with the Elastic APM .NET Agent. If your application uses the Microsoft.AspNetCore.All package the easiest way to reference the Elastic APM project is to use this package. If you only need specific functionalities (e.g. EF Core monitoring, or ASP.NET Core without EF Core monitoring, etc) you can reference specific Elastic.Apm packages. See: https://github.com/elastic/apm-agent-dotnet/tree/main/docs diff --git a/src/profiler/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj b/src/profiler/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj index cc9b62bf3..ac11a7546 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj +++ b/src/profiler/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj @@ -1,7 +1,7 @@ - net462;netstandard2.0;net6.0 + net462;netstandard2.0;net8.0 false diff --git a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs index e4c1ba5c2..06e95ca64 100644 --- a/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs +++ b/src/profiler/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs @@ -66,7 +66,7 @@ public static partial class DuckType private static Dictionary> _ignoresAccessChecksToAssembliesSetDictionary = new Dictionary>(); #pragma warning restore IDE0044, IDE1006 - + internal static long AssemblyCount => _assemblyCount; internal static long TypeCount => _typeCount; diff --git a/test/Elastic.Apm.Tests/LoggerTests.cs b/test/Elastic.Apm.Tests/LoggerTests.cs index 10a02acb4..1be429dd6 100644 --- a/test/Elastic.Apm.Tests/LoggerTests.cs +++ b/test/Elastic.Apm.Tests/LoggerTests.cs @@ -156,7 +156,7 @@ public void EnsureFormattersAreShared() var scopedLogger = consoleLogger.Scoped("MyTestScope"); for (var i = 0; i < 10; i++) scopedLogger.Warning()?.Log("This is a test log from the test StructuredLogTemplateWith1MissingArgument, args: {arg1}", i); -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER var cachedFormatterCount = scopedLogger.Formatters.Count(); cachedFormatterCount.Should().Be(1); diff --git a/test/azure/Elastic.Apm.Azure.Functions.Tests/AzureFunctionsInProcessTests.cs b/test/azure/Elastic.Apm.Azure.Functions.Tests/AzureFunctionsInProcessTests.cs index 639ec31fc..2522faa98 100644 --- a/test/azure/Elastic.Apm.Azure.Functions.Tests/AzureFunctionsInProcessTests.cs +++ b/test/azure/Elastic.Apm.Azure.Functions.Tests/AzureFunctionsInProcessTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Threading.Tasks; +using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; using Xunit; using Xunit.Abstractions; @@ -17,8 +18,7 @@ public class AzureFunctionsInProcessTests : AzureFunctionsTestBase, IClassFixtur public AzureFunctionsInProcessTests(ITestOutputHelper output, InProcessContext context) : base(output, context) { } - - [Fact] + [FlakyCiTestFact(2501)] public async Task Invoke_Http_Ok() { var transaction = await InvokeAndAssertFunction(SampleHttpTrigger); @@ -28,7 +28,7 @@ public async Task Invoke_Http_Ok() transaction.Context.Response.StatusCode.Should().Be(200); } - [Fact] + [FlakyCiTestFact(2501)] public async Task Invoke_Http_InternalServerError() { var transaction = await InvokeAndAssertFunction(HttpTriggerWithInternalServerError); @@ -38,7 +38,7 @@ public async Task Invoke_Http_InternalServerError() transaction.Context.Response.StatusCode.Should().Be(500); } - [Fact] + [FlakyCiTestFact(2501)] public async Task Invoke_Http_FunctionThrowsException() { var transaction = await InvokeAndAssertFunction(HttpTriggerWithException); @@ -49,7 +49,7 @@ public async Task Invoke_Http_FunctionThrowsException() transaction.Context.Response.StatusCode.Should().Be(500); } - [Fact] + [FlakyCiTestFact(2501)] public async Task Invoke_Http_NotFound() { var transaction = await InvokeAndAssertFunction(HttpTriggerWithNotFound); diff --git a/test/azure/applications/Elastic.Apm.AzureFunctionApp.Core/Elastic.Apm.AzureFunctionApp.Core.csproj b/test/azure/applications/Elastic.Apm.AzureFunctionApp.Core/Elastic.Apm.AzureFunctionApp.Core.csproj index 7728c0570..5850f50c5 100644 --- a/test/azure/applications/Elastic.Apm.AzureFunctionApp.Core/Elastic.Apm.AzureFunctionApp.Core.csproj +++ b/test/azure/applications/Elastic.Apm.AzureFunctionApp.Core/Elastic.Apm.AzureFunctionApp.Core.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0 enable enable false diff --git a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj index 8bb50ce7e..54fda31ea 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj +++ b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/Elastic.AzureFunctionApp.InProcess.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 v4 Library enable diff --git a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/local.settings.json b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/local.settings.json index 320edb1e3..fd2cd88b4 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.InProcess/local.settings.json +++ b/test/azure/applications/Elastic.AzureFunctionApp.InProcess/local.settings.json @@ -5,7 +5,8 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet", "FUNCTIONS_EXTENSION_VERSION": "4", "WEBSITE_OWNER_NAME": "abcd1234-abcd-acdc-1234-112233445566+testfaas_group-CentralUSwebspace-Linux", - "WEBSITE_SITE_NAME": "testfaas" + "WEBSITE_SITE_NAME": "testfaas", + "FUNCTIONS_INPROC_NET8_ENABLED": "1" // Include this breaks on the latest version of func - Excluding it for now. //"WEBSITE_INSTANCE_ID": "20367ea8-70b9-41b4-a552-b2a826b3aa0b" }, diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs index 10f38113d..137eeb935 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs @@ -89,9 +89,7 @@ public async Task HomeSimplePageTransactionTest() var aspNetCoreVersion = Assembly.Load("Microsoft.AspNetCore").GetName().Version.ToString(); agent.Service.Framework.Version.Should().Be(aspNetCoreVersion); -#if NET6_0 - agent.Service.Runtime.Name.Should().Be(Runtime.DotNetName + " 6"); -#elif NET8_0 +#if NET8_0 agent.Service.Runtime.Name.Should().Be(Runtime.DotNetName + " 8"); #else agent.Service.Runtime.Name.Should().Be(Runtime.DotNetCoreName); diff --git a/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj b/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj index 09e04e82c..758744247 100644 --- a/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj +++ b/test/integrations/applications/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0 false AnyCPU @@ -16,14 +16,6 @@ - - - - - - - - diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj b/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj index 2000ec119..cf1be170e 100644 --- a/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj +++ b/test/integrations/applications/SampleConsoleNetCoreApp/SampleConsoleNetCoreApp.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net8.0 + net8.0 diff --git a/test/integrations/applications/WebApiSample/WebApiSample.csproj b/test/integrations/applications/WebApiSample/WebApiSample.csproj index 64c89df05..1c3b77682 100644 --- a/test/integrations/applications/WebApiSample/WebApiSample.csproj +++ b/test/integrations/applications/WebApiSample/WebApiSample.csproj @@ -1,7 +1,7 @@  - net6.0;net8.0 + net8.0 false diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs index 01379b4ac..5da7a6574 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs @@ -20,7 +20,6 @@ public IEnumerator GetEnumerator() { // TODO: Add x64/x86 options. macOS and Linux do not support x86 yield return new object[] { "net8.0" }; - yield return new object[] { "net6.0" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs index c49190903..69679eddf 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs @@ -7,7 +7,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER using System; using System.Threading; using System.Threading.Tasks; @@ -23,7 +23,7 @@ public static TReturn OnAsyncMethodEnd(TTarget instance, TRetu returnValue; [Fact] - public async ValueTask SuccessTest() + public async Task SuccessTest() { var tcg = new ValueTaskContinuationGenerator(); var cTask = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs index 77ef2078f..621f9132a 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs @@ -25,7 +25,7 @@ public void NonPublicStructCopyTest() Assert.Equal(ValuesDuckType.Third.ToString(), ((IGetValue)copy).GetValueProp); } -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER [Fact] public void NonPublicStructInterfaceProxyTest() { diff --git a/test/profiler/applications/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj b/test/profiler/applications/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj index cced4bb1e..9d1026735 100644 --- a/test/profiler/applications/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj +++ b/test/profiler/applications/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj @@ -1,7 +1,7 @@ - net462;net6.0;net8.0 + net462;net8.0 diff --git a/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj b/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj index 35ffd8a2f..8f69d0010 100644 --- a/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj +++ b/test/profiler/applications/MySqlDataSample/MySqlDataSample.csproj @@ -2,7 +2,7 @@ Exe - net462;net6.0;net8.0 + net462;net8.0 diff --git a/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj b/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj index 627143054..7b43bb7ac 100644 --- a/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj +++ b/test/profiler/applications/NpgsqlSample/NpgsqlSample.csproj @@ -3,7 +3,7 @@ 5.0.18 Exe - net462;net6.0;net8.0 + net462;net8.0 diff --git a/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj b/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj index e795de6d9..3d4993ad8 100644 --- a/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj +++ b/test/profiler/applications/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net8.0 + net8.0 diff --git a/test/profiler/applications/SqlClientSample/SqlClientSample.csproj b/test/profiler/applications/SqlClientSample/SqlClientSample.csproj index 72841a793..0de2ff849 100644 --- a/test/profiler/applications/SqlClientSample/SqlClientSample.csproj +++ b/test/profiler/applications/SqlClientSample/SqlClientSample.csproj @@ -3,7 +3,7 @@ 4.8.6 Exe - net462;net6.0;net8.0 + net462;net8.0 diff --git a/test/profiler/applications/SqliteSample/SqliteSample.csproj b/test/profiler/applications/SqliteSample/SqliteSample.csproj index 1169e0dde..2fed60f08 100644 --- a/test/profiler/applications/SqliteSample/SqliteSample.csproj +++ b/test/profiler/applications/SqliteSample/SqliteSample.csproj @@ -3,7 +3,7 @@ 8.0.2 Exe - net462;net6.0;net8.0 + net462;net8.0 x64 diff --git a/test/startuphook/Elastic.Apm.StartupHook.Sample/Elastic.Apm.StartupHook.Sample.csproj b/test/startuphook/Elastic.Apm.StartupHook.Sample/Elastic.Apm.StartupHook.Sample.csproj index b7bbc052d..ccd7e8ece 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Sample/Elastic.Apm.StartupHook.Sample.csproj +++ b/test/startuphook/Elastic.Apm.StartupHook.Sample/Elastic.Apm.StartupHook.Sample.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0 false diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs index b75fd8a64..43cb28b3a 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs @@ -9,7 +9,10 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Xml.Linq; using ProcNet; +using ProcNet.Std; +using Xunit.Abstractions; namespace Elastic.Apm.StartupHook.Tests { @@ -19,14 +22,16 @@ namespace Elastic.Apm.StartupHook.Tests public class DotnetProject : IDisposable { private readonly string _publishDirectory = Path.Combine("bin", "Publish"); + private readonly ITestOutputHelper _output; private ObservableProcess _process; - private DotnetProject(string name, string template, string framework, string directory) + private DotnetProject(string name, string template, string framework, string directory, ITestOutputHelper output) { Name = name; Template = template; Framework = framework; Directory = directory; + _output = output; } /// @@ -49,24 +54,42 @@ private DotnetProject(string name, string template, string framework, string dir /// public string Name { get; } - private void Publish() + private bool TryPublish() { + var workingDirectory = Path.Combine(Directory, Name); + try { - var processInfo = new ProcessStartInfo + _output.WriteLine("Publishing {0} to {1}.", Name, _publishDirectory); + + var args = new[] + { + "publish", + "-c", "Release", + "--output", _publishDirectory + }; + + _output.WriteLine("Running 'dotnet {0}' in {1}", string.Join(' ', args), workingDirectory); + + var startArgs = new StartArguments("dotnet", args) { - FileName = "dotnet", - Arguments = $"publish -c Release --property:PublishDir={_publishDirectory}", - WorkingDirectory = Directory + WorkingDirectory = workingDirectory }; - using var process = new Process { StartInfo = processInfo }; - process.Start(); - process.WaitForExit(); + var publishResult = Proc.Start(startArgs, TimeSpan.FromSeconds(30)); + + foreach (var line in publishResult.ConsoleOut) + { + _output.WriteLine(line.Line); + } + + _output.WriteLine("Publish exited with exit code: {0}", publishResult.ExitCode ?? -1); + + return publishResult.ExitCode.HasValue && publishResult.ExitCode.Value == 0; } catch (Exception e) { - throw new Exception($"Problem running dotnet publish in {Directory} to output to {_publishDirectory}", e); + throw new Exception($"Problem running dotnet publish in {workingDirectory} to output to {_publishDirectory}.", e); } } @@ -78,19 +101,32 @@ private void Publish() /// public ObservableProcess CreateProcess(string startupHookZipPath, IDictionary environmentVariables = null) { - Publish(); + if (!TryPublish()) + throw new Exception("Unable to publish sample application."); var startupHookAssembly = UnzipStartupHook(startupHookZipPath); + + _output.WriteLine("Unzipped startup hooks to {0}.", startupHookAssembly); + environmentVariables ??= new Dictionary(); environmentVariables["DOTNET_STARTUP_HOOKS"] = startupHookAssembly; + + var workingDir = Path.Combine(Directory, Name, _publishDirectory); + + _output.WriteLine("Launching process 'dotnet {0}.dll' from {1}", Name, workingDir); + var arguments = new StartArguments("dotnet", $"{Name}.dll") { - WorkingDirectory = Path.Combine(Directory, _publishDirectory), + WorkingDirectory = workingDir, SendControlCFirst = true, Environment = environmentVariables }; _process = new ObservableProcess(arguments); + + if (_process.ExitCode.HasValue) + _output.WriteLine("Launching process 'dotnet {0}.dll' failed with exit code {1}", Name, _process.ExitCode.Value); + return _process; } @@ -118,20 +154,47 @@ private static string UnzipStartupHook(string startupHookZipPath) public void Dispose() => _process?.Dispose(); - public static DotnetProject Create(string name, string template, string framework, params string[] arguments) + public static DotnetProject Create(ITestOutputHelper output, string name, string template, string framework, params string[] arguments) { var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + System.IO.Directory.CreateDirectory(directory); + + output.WriteLine("Using temp directory '{0}'", directory); + + var globalJsonCreationResult = Proc.Start(new StartArguments("dotnet", + [ + "new", + "globaljson", + "--sdk-version", + "8.0.404", // Fixing this specific version, for now + "--roll-forward", + "disable" + ]) + { + WorkingDirectory = directory + }); + + foreach (var line in globalJsonCreationResult.ConsoleOut) + { + output.WriteLine(line.Line); + } + var args = new[] { "new", template, "--name", name, - "--output", $"\"{directory}\"", "--no-update-check", "--framework", framework }.Concat(arguments); - var result = Proc.Start(new StartArguments("dotnet", args)); + var argsString = string.Join(' ', args); + output.WriteLine("Running dotnet {0}", argsString); + + var result = Proc.Start(new StartArguments("dotnet", args) + { + WorkingDirectory = directory + }, TimeSpan.FromSeconds(30)); if (result.Completed) { @@ -149,7 +212,14 @@ public static DotnetProject Create(string name, string template, string framewor + $"output: {string.Join(Environment.NewLine, result.ConsoleOut.Select(c => c.Line))}"); } - return new DotnetProject(name, template, framework, directory); + output.WriteLine("Created new {0} project for {1} in {2}.", template, framework, directory); + + foreach (var line in result.ConsoleOut) + { + output.WriteLine(line.Line); + } + + return new DotnetProject(name, template, framework, directory, output); } } } diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/StartupHookTests.cs b/test/startuphook/Elastic.Apm.StartupHook.Tests/StartupHookTests.cs index 4b2c2b733..8810d7bee 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/StartupHookTests.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/StartupHookTests.cs @@ -16,18 +16,20 @@ using Elastic.Apm.Tests.Utilities; using FluentAssertions; using Xunit; +using Xunit.Abstractions; using static Elastic.Apm.Config.ConfigurationOption; using Environment = System.Environment; using LogLevel = Elastic.Apm.Logging.LogLevel; namespace Elastic.Apm.StartupHook.Tests { - public class StartupHookTests + public partial class StartupHookTests(ITestOutputHelper output) { + private readonly ITestOutputHelper _output = output; + // NOTE: We test the two latest supported LTS releases. private static IEnumerable<(string TargetFramework, string RuntimeName, string Version, string ShortVersion)> GetDotNetFrameworkVersionInfos() { - yield return ("net6.0", ".NET 6", "6.0.0.0", "60"); yield return ("net8.0", ".NET 8", "8.0.0.0", "80"); } @@ -211,91 +213,97 @@ public async Task Auto_Instrument_With_StartupHook(string template, string name, metadataWaitHandle.Set(); }; + using var project = DotnetProject.Create(_output, name, template, targetFramework, "--no-https"); - using var project = DotnetProject.Create(name, template, targetFramework, "--no-https"); var environmentVariables = new Dictionary { [ServerUrl.ToEnvironmentVariable()] = $"http://localhost:{port}", [CloudProvider.ToEnvironmentVariable()] = "none" }; - using (var process = project.CreateProcess(SolutionPaths.AgentZip, environmentVariables)) - { - var startHandle = new ManualResetEvent(false); - Uri uri = null; - ExceptionDispatchInfo e = null; - var capturedLines = new List(); - var endpointRegex = new Regex(@"\s*Now listening on:\s*(?http\:[^\s]*)"); - - process.SubscribeLines( - line => + using var process = project.CreateProcess(SolutionPaths.AgentZip, environmentVariables); + + if (process.ExitCode.HasValue && process.ExitCode.Value != 0) + throw new Exception($"Unable to start instrumented .NET application {name}"); + + var startHandle = new ManualResetEvent(false); + Uri uri = null; + ExceptionDispatchInfo e = null; + var capturedLines = new List(); + var endpointRegex = NowListeningOnRegex(); + + process.SubscribeLines( + line => + { + capturedLines.Add(line.Line); + var match = endpointRegex.Match(line.Line); + if (match.Success) { - capturedLines.Add(line.Line); - var match = endpointRegex.Match(line.Line); - if (match.Success) + try + { + var endpoint = match.Groups["endpoint"].Value.Trim(); + uri = new UriBuilder(endpoint) { Path = path }.Uri; + } + catch (Exception exception) { - try - { - var endpoint = match.Groups["endpoint"].Value.Trim(); - uri = new UriBuilder(endpoint) { Path = path }.Uri; - } - catch (Exception exception) - { - e = ExceptionDispatchInfo.Capture(exception); - } - - startHandle.Set(); + e = ExceptionDispatchInfo.Capture(exception); } - }, - exception => e = ExceptionDispatchInfo.Capture(exception)); - var timeout = TimeSpan.FromMinutes(2); - var signalled = startHandle.WaitOne(timeout); - if (!signalled) - { - throw new Exception($"Could not start dotnet project within timeout {timeout}: " - + string.Join(Environment.NewLine, capturedLines)); - } + startHandle.Set(); + } + }, + exception => e = ExceptionDispatchInfo.Capture(exception)); - e?.Throw(); + var timeout = TimeSpan.FromMinutes(1); + var signalled = startHandle.WaitOne(timeout); - var client = new HttpClient(); - var response = await client.GetAsync(uri); + if (!signalled) + { + throw new Exception($"Could not start dotnet project within timeout {timeout}: " + + string.Join(Environment.NewLine, capturedLines)); + } - response.IsSuccessStatusCode.Should().BeTrue(); + e?.Throw(); - signalled = transactionWaitHandle.WaitOne(timeout); - if (!signalled) - { - throw new Exception($"Did not receive transaction within timeout {timeout}: " - + string.Join(Environment.NewLine, capturedLines) - + Environment.NewLine - + string.Join(Environment.NewLine, apmLogger.Lines)); - } + var client = new HttpClient(); + var response = await client.GetAsync(uri); - apmServer.ReceivedData.Transactions.Should().HaveCount(1); + response.IsSuccessStatusCode.Should().BeTrue(); - var transaction = apmServer.ReceivedData.Transactions.First(); - transaction.Name.Should().NotBeNullOrEmpty(); + signalled = transactionWaitHandle.WaitOne(timeout); + if (!signalled) + { + throw new Exception($"Did not receive transaction within timeout {timeout}: " + + string.Join(Environment.NewLine, capturedLines) + + Environment.NewLine + + string.Join(Environment.NewLine, apmLogger.Lines)); + } - signalled = metadataWaitHandle.WaitOne(timeout); - if (!signalled) - { - throw new Exception($"Did not receive metadata within timeout {timeout}: " - + string.Join(Environment.NewLine, capturedLines) - + Environment.NewLine - + string.Join(Environment.NewLine, apmLogger.Lines)); - } + apmServer.ReceivedData.Transactions.Should().HaveCount(1); - apmServer.ReceivedData.Metadata.Should().HaveCountGreaterOrEqualTo(1); - var metadata = apmServer.ReceivedData.Metadata.First(); - metadata.Service.Agent.ActivationMethod.Should().Be(Consts.ActivationMethodStartupHook); - metadata.Service.Runtime.Name.Should().NotBeNullOrEmpty(); - metadata.Service.Framework.Name.Should().Be("ASP.NET Core"); - metadata.Service.Framework.Version.Should().NotBeNullOrEmpty(); + var transaction = apmServer.ReceivedData.Transactions.First(); + transaction.Name.Should().NotBeNullOrEmpty(); + + signalled = metadataWaitHandle.WaitOne(timeout); + if (!signalled) + { + throw new Exception($"Did not receive metadata within timeout {timeout}: " + + string.Join(Environment.NewLine, capturedLines) + + Environment.NewLine + + string.Join(Environment.NewLine, apmLogger.Lines)); } + apmServer.ReceivedData.Metadata.Should().HaveCountGreaterOrEqualTo(1); + var metadata = apmServer.ReceivedData.Metadata.First(); + metadata.Service.Agent.ActivationMethod.Should().Be(Consts.ActivationMethodStartupHook); + metadata.Service.Runtime.Name.Should().NotBeNullOrEmpty(); + metadata.Service.Framework.Name.Should().Be("ASP.NET Core"); + metadata.Service.Framework.Version.Should().NotBeNullOrEmpty(); + await apmServer.StopAsync(); } + + [GeneratedRegex(@"\s*Now listening on:\s*(?http\:[^\s]*)")] + private static partial Regex NowListeningOnRegex(); } } From 1adae59b1878ae6a6e0ef061a7070c0822a5c755 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 28 Nov 2024 12:05:52 +0000 Subject: [PATCH 54/77] Phase one logger optimisations (#2503) This PR dramatically reduces the overhead of scoped logging, even for scenarios where the configured log level verbosity was low (e.g., Warning). The most critical improvement is caching scoped loggers within Spans, Traces, and Errors. We lose convenience in no longer having the span ID in the scope, but it's generally available from the Span argument. Previously, a new `ScopedLogger` was created for every span and transaction, and a new `ConditionalWeakTable` was also allocated. Due to the use of Finalisers on the CWT, this had an even more significant GC impact. We now maintain a cache of scoped loggers (of which there are relatively few). The ID creation for the Spans has been switched to `ActivitySpanId.CreateRandom().ToString()` from the MS diagnostics library, as this has been more heavily optimised than our original version and achieves the same goal. In `Tracer.End()`, we used an `Assertion` with a large interpolated string. This was being interpolated for each transaction as the `Assertion` path was enabled. However, the condition we assert is generally not met. Therefore, we now check this manually to only create the message if we are in an invalid state. I've also switched two structs to record structs, as these were used as keys in dictionaries and ended up being boxed due to a lack of `IEquality` implementations. These are phase one of optimisations for the logging implementation and address the general production scenario of using a less verbose log level. ### Profiling Profiling was achieved using a .NET 9 ASP.NET Core minimal API project with 20 concurrent connections and 100,000 requests. **Before Memory Profile** ![image](https://github.com/user-attachments/assets/6ba52339-dc4e-44a0-811d-039c9807c0fb) **After Memory Profile** ![image](https://github.com/user-attachments/assets/22a8fdcc-3cab-4b83-bd27-b6107f849899) **Before Top Retained** ![image](https://github.com/user-attachments/assets/348c837e-a910-4220-8a60-e12b4fd3f961) **After Top Retained** ![image](https://github.com/user-attachments/assets/d7a31e7b-f5cf-419a-8eaa-ed4042a07470) Closes #2500 --- src/Elastic.Apm/Api/Tracer.cs | 4 +- ...ppSettingsConfigurationKeyValueProvider.cs | 8 +-- .../Logging/IApmLoggingExtensions.cs | 66 +++++++++++++++---- src/Elastic.Apm/Logging/ScopedLogger.cs | 2 +- .../BreakdownMetricsProvider.cs | 2 +- src/Elastic.Apm/Model/Error.cs | 14 ++-- src/Elastic.Apm/Model/Span.cs | 51 +++++++------- src/Elastic.Apm/Model/SpanTimerKey.cs | 10 +-- src/Elastic.Apm/Model/Transaction.cs | 52 ++++++++------- .../OpenTelemetry/ElasticActivityListener.cs | 25 ++++--- .../AzureFunctionsContext.cs | 2 +- 11 files changed, 139 insertions(+), 97 deletions(-) diff --git a/src/Elastic.Apm/Api/Tracer.cs b/src/Elastic.Apm/Api/Tracer.cs index 5c810dbf9..d91fc97ee 100644 --- a/src/Elastic.Apm/Api/Tracer.cs +++ b/src/Elastic.Apm/Api/Tracer.cs @@ -21,7 +21,7 @@ internal class Tracer : ITracer { private readonly IApmServerInfo _apmServerInfo; private readonly IConfigurationSnapshotProvider _configurationProvider; - private readonly ScopedLogger _logger; + private readonly IApmLogger _logger; private readonly IPayloadSender _sender; private readonly Service _service; private readonly BreakdownMetricsProvider _breakdownMetricsProvider; @@ -83,7 +83,7 @@ private Transaction StartTransactionInternal(string name, string type, Distribut traceId: traceId, links: links, current: current) { Service = _service }; - _logger.Debug()?.Log("Starting {TransactionValue}", retVal); + _logger?.Debug()?.Log("Starting {TransactionValue}", retVal); return retVal; } diff --git a/src/Elastic.Apm/Config/Net4FullFramework/AppSettingsConfigurationKeyValueProvider.cs b/src/Elastic.Apm/Config/Net4FullFramework/AppSettingsConfigurationKeyValueProvider.cs index 4f7d18c21..84503d367 100644 --- a/src/Elastic.Apm/Config/Net4FullFramework/AppSettingsConfigurationKeyValueProvider.cs +++ b/src/Elastic.Apm/Config/Net4FullFramework/AppSettingsConfigurationKeyValueProvider.cs @@ -3,8 +3,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -#if NETFRAMEWORK #nullable enable +#if NETFRAMEWORK using System.Configuration; using Elastic.Apm.Logging; @@ -14,8 +14,7 @@ internal class AppSettingsConfigurationKeyValueProvider : IConfigurationKeyValue { private readonly IApmLogger? _logger; - public AppSettingsConfigurationKeyValueProvider(IApmLogger? logger) => - _logger = logger?.Scoped(nameof(AppSettingsConfigurationKeyValueProvider)); + public AppSettingsConfigurationKeyValueProvider(IApmLogger? logger) => _logger = logger?.Scoped(nameof(AppSettingsConfigurationKeyValueProvider)); public string Description => nameof(AppSettingsConfigurationKeyValueProvider); @@ -29,9 +28,10 @@ public AppSettingsConfigurationKeyValueProvider(IApmLogger? logger) => } catch (ConfigurationErrorsException ex) { - _logger.Error()?.LogException(ex, "Exception thrown from ConfigurationManager.AppSettings - falling back on environment variables"); + _logger?.Error()?.LogException(ex, "Exception thrown from ConfigurationManager.AppSettings - falling back on environment variables"); } return null; } } #endif +#nullable restore diff --git a/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs b/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs index 4b9b4521c..9aa11559a 100644 --- a/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs +++ b/src/Elastic.Apm/Logging/IApmLoggingExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -12,13 +13,53 @@ namespace Elastic.Apm.Logging; +#nullable enable internal static class LoggingExtensions { - private static ConditionalWeakTable Formatters { get; } = new(); + // Using a ConcurrentDictionary rather than ConditionalWeakTable as we expect few distinct scopes + // and we want to retain them for reuse across the application lifetime. We use the scope name and the + // instance of the base logger for the key, for rare scenarios when different base loggers might be + // used. In reality, this only seems to affect testing scenarios. + private static readonly ConcurrentDictionary<(string, IApmLogger), ScopedLogger> ScopedLoggers = new(); - internal static ScopedLogger Scoped(this IApmLogger logger, string scope) => new(logger is ScopedLogger s ? s.Logger : logger, scope); + private static readonly ConditionalWeakTable Formatters = new(); - private static void DoLog(this IApmLogger logger, LogLevel level, string message, Exception e, params object[] args) + /// + /// Returns a ScopedLogger, potentially from the cache. + /// + /// An existing . + /// The name of the scope. + /// A new scoped logger or null of the is null. + /// Requires to be non-null and non-empty. + internal static ScopedLogger? Scoped(this IApmLogger? logger, string scope) + { + if (string.IsNullOrEmpty(scope)) + throw new ArgumentException("Scope is required to be non-null and non-empty.", nameof(scope)); + + if (logger is null) + return null; + + var baseLogger = logger is ScopedLogger s ? s.Logger : logger; + + if (!ScopedLoggers.TryGetValue((scope, baseLogger), out var scopedLogger)) + { + // Ensure we don't allow creations of scoped loggers 'wrapping' other scoped loggers + var potentialScopedLogger = new ScopedLogger(baseLogger, scope); + + if (ScopedLoggers.TryAdd((scope, baseLogger), potentialScopedLogger)) + { + scopedLogger = potentialScopedLogger; + } + else + { + scopedLogger = ScopedLoggers[(scope, baseLogger)]; + } + } + + return scopedLogger; + } + + private static void DoLog(this IApmLogger logger, LogLevel level, string message, Exception? exception, params object?[]? args) { try { @@ -28,9 +69,9 @@ private static void DoLog(this IApmLogger logger, LogLevel level, string message var logValues = formatter.GetState(args); - logger?.Log(level, logValues, e, static (l, _) => l.Formatter.Format(l.Args)); + logger?.Log(level, logValues, exception, static (l, _) => l.Formatter.Format(l.Args)); } - catch (Exception exception) + catch (Exception ex) { // For now we will just print it to System.Diagnostics.Trace // In the future we should consider error counters to increment and log periodically on a worker thread @@ -44,12 +85,12 @@ private static void DoLog(this IApmLogger logger, LogLevel level, string message System.Diagnostics.Trace.WriteLine("Elastic APM .NET Agent: [CRITICAL] Exception thrown by logging implementation." + $" Log message: `{message.AsNullableToString()}'." - + $" args.Length: {args.Length}." + + $" args.Length: {args?.Length ?? 0}." + $" Current thread: {DbgUtils.CurrentThreadDesc}" + newLine - + $"+-> Exception (exception): {exception.GetType().FullName}: {exception.Message}{newLine}{exception.StackTrace}" - + (e != null - ? newLine + $"+-> Exception (e): {e.GetType().FullName}: {e.Message}{newLine}{e.StackTrace}" + + $"+-> Exception (exception): {ex.GetType().FullName}: {ex.Message}{newLine}{ex.StackTrace}" + + (exception != null + ? newLine + $"+-> Exception (e): {exception.GetType().FullName}: {exception.Message}{newLine}{exception.StackTrace}" : $"e: {ObjectExtensions.NullAsString}") + newLine + "+-> Current stack trace:" + currentStackTrace @@ -62,11 +103,11 @@ private static void DoLog(this IApmLogger logger, LogLevel level, string message } } -#if !NET6_0_OR_GREATER +#if !NET8_0_OR_GREATER private static readonly object _lock = new(); #endif - private static LogValuesFormatter GetOrAddFormatter(string message, IReadOnlyCollection args) + private static LogValuesFormatter GetOrAddFormatter(string message, IReadOnlyCollection? args) { if (Formatters.TryGetValue(message, out var formatter)) return formatter; @@ -162,7 +203,7 @@ internal readonly struct MaybeLogger public MaybeLogger(IApmLogger logger, LogLevel level) => (_logger, _level) = (logger, level); - public void Log(string message, params object[] args) => _logger.DoLog(_level, message, null, args); + public void Log(string message, params object?[]? args) => _logger.DoLog(_level, message, null, args); public void LogException(Exception exception, string message, params object[] args) => _logger.DoLog(_level, message, exception, args); @@ -179,3 +220,4 @@ public void LogExceptionWithCaller(Exception exception, } } } +#nullable restore diff --git a/src/Elastic.Apm/Logging/ScopedLogger.cs b/src/Elastic.Apm/Logging/ScopedLogger.cs index ed49289f1..1ad8d35ed 100644 --- a/src/Elastic.Apm/Logging/ScopedLogger.cs +++ b/src/Elastic.Apm/Logging/ScopedLogger.cs @@ -16,7 +16,7 @@ internal class ScopedLogger : IApmLogger public IApmLogger Logger { get; } -#if !NET6_0_OR_GREATER +#if !NET8_0_OR_GREATER private readonly object _lock = new(); #endif diff --git a/src/Elastic.Apm/Metrics/MetricsProvider/BreakdownMetricsProvider.cs b/src/Elastic.Apm/Metrics/MetricsProvider/BreakdownMetricsProvider.cs index 576edc21b..c2214fc38 100644 --- a/src/Elastic.Apm/Metrics/MetricsProvider/BreakdownMetricsProvider.cs +++ b/src/Elastic.Apm/Metrics/MetricsProvider/BreakdownMetricsProvider.cs @@ -17,7 +17,7 @@ internal class BreakdownMetricsProvider : IMetricsProvider /// /// Encapsulates types which are used as key to group MetricSets. /// - private struct GroupKey + private readonly record struct GroupKey { public TransactionInfo Transaction { get; } public SpanInfo Span { get; } diff --git a/src/Elastic.Apm/Model/Error.cs b/src/Elastic.Apm/Model/Error.cs index f2c15f8db..6aa8fe7ae 100644 --- a/src/Elastic.Apm/Model/Error.cs +++ b/src/Elastic.Apm/Model/Error.cs @@ -20,16 +20,14 @@ internal class Error : IError [JsonIgnore] internal IConfiguration Configuration { get; } - public Error(CapturedException capturedException, Transaction transaction, string parentId, IApmLogger loggerArg, Dictionary labels = null - ) - : this(transaction, parentId, loggerArg, labels) => Exception = capturedException; + public Error(CapturedException capturedException, Transaction transaction, string parentId, IApmLogger loggerArg, Dictionary labels = null) : + this(transaction, parentId, loggerArg, labels) => Exception = capturedException; public Error(ErrorLog errorLog, Transaction transaction, string parentId, IApmLogger loggerArg, Dictionary labels = null) : this(transaction, parentId, loggerArg, labels) - => Log = errorLog; - + => Log = errorLog; - private Error(Transaction transaction, string parentId, IApmLogger loggerArg, Dictionary labels = null) + private Error(Transaction transaction, string parentId, IApmLogger logger, Dictionary labels = null) { Timestamp = TimeUtils.TimestampNow(); Id = RandomGenerator.GenerateRandomBytesAsString(new byte[16]); @@ -57,9 +55,7 @@ private Error(Transaction transaction, string parentId, IApmLogger loggerArg, Di CheckAndCaptureBaggage(transaction); - IApmLogger logger = loggerArg?.Scoped($"{nameof(Error)}.{Id}"); - logger.Trace() - ?.Log("New Error instance created: {Error}. Time: {Time} (as timestamp: {Timestamp})", + logger?.Scoped(nameof(Error))?.Trace()?.Log("New Error instance created: {Error}. Time: {Time} (as timestamp: {Timestamp})", this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp); } diff --git a/src/Elastic.Apm/Model/Span.cs b/src/Elastic.Apm/Model/Span.cs index 9aff679cb..707c9f45d 100644 --- a/src/Elastic.Apm/Model/Span.cs +++ b/src/Elastic.Apm/Model/Span.cs @@ -72,16 +72,16 @@ public Span(string name, Activity current = null ) { - InstrumentationFlag = instrumentationFlag; - Timestamp = timestamp ?? TimeUtils.TimestampNow(); - Id = id ?? RandomGenerator.GenerateRandomBytesAsString(new byte[8]); - _logger = logger?.Scoped($"{nameof(Span)}.{Id}"); - + _logger = logger?.Scoped(nameof(Span)); _payloadSender = payloadSender; _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer; _parentSpan = parentSpan; _enclosingTransaction = enclosingTransaction; _apmServerInfo = apmServerInfo; + + InstrumentationFlag = instrumentationFlag; + Timestamp = timestamp ?? TimeUtils.TimestampNow(); + Id = id ?? ActivitySpanId.CreateRandom().ToString(); IsExitSpan = isExitSpan; Name = name; Type = type; @@ -123,9 +123,10 @@ public Span(string name, _currentExecutionSegmentsContainer.CurrentSpan = this; - _logger.Trace() - ?.Log("New Span instance created: {Span}. Start time: {Time} (as timestamp: {Timestamp}). Parent span: {Span}", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, _parentSpan); + var formattedTimestamp = _logger.IsEnabled(LogLevel.Trace) ? TimeUtils.FormatTimestampForLog(Timestamp) : string.Empty; + + _logger?.Trace()?.Log("New Span instance created: {Span}. Start time: {Time} (as timestamp: {Timestamp}). Parent span: {Span}", + this, formattedTimestamp, Timestamp, _parentSpan); } private void CheckAndCaptureBaggage() @@ -138,7 +139,7 @@ private void CheckAndCaptureBaggage() if (!WildcardMatcher.IsAnyMatch(Configuration.BaggageToAttach, baggage.Key)) continue; - Otel ??= new OTel { Attributes = new Dictionary() }; + Otel ??= new OTel { Attributes = [] }; var newKey = $"baggage.{baggage.Key}"; Otel.Attributes[newKey] = baggage.Value; @@ -392,17 +393,18 @@ internal Span StartSpanInternal(string name, string type, string subType = null, string id = null, bool isExitSpan = false, IEnumerable links = null, Activity current = null ) { - var retVal = new Span(name, type, Id, TraceId, _enclosingTransaction, _payloadSender, _logger, _currentExecutionSegmentsContainer, + var span = new Span(name, type, Id, TraceId, _enclosingTransaction, _payloadSender, _logger, _currentExecutionSegmentsContainer, _apmServerInfo, this, instrumentationFlag, captureStackTraceOnStart, timestamp, isExitSpan, id, links, current: current); if (!string.IsNullOrEmpty(subType)) - retVal.Subtype = subType; + span.Subtype = subType; if (!string.IsNullOrEmpty(action)) - retVal.Action = action; + span.Action = action; - _logger.Trace()?.Log("Starting {SpanDetails}", retVal.ToString()); - return retVal; + _logger?.Trace()?.Log("Starting {SpanDetails}", span.ToString()); + + return span; } /// @@ -416,12 +418,13 @@ public void End() if (Outcome == Outcome.Unknown && !_outcomeChangedThroughApi) Outcome = Outcome.Success; + var formattedTimestamp = _logger.IsEnabled(LogLevel.Trace) ? TimeUtils.FormatTimestampForLog(Timestamp) : string.Empty; + if (Duration.HasValue) { - _logger.Trace() - ?.Log("Ended {Span} (with Duration already set)." + + _logger?.Trace()?.Log("Ended {Span} (with Duration already set)." + " Start time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, Duration); + this, formattedTimestamp, Timestamp, Duration); if (_parentSpan != null) _parentSpan?._childDurationTimer.OnChildEnd((long)(Timestamp + Duration.Value * 1000)); @@ -448,10 +451,9 @@ public void End() _childDurationTimer.OnSpanEnd(endTimestamp); - _logger.Trace() - ?.Log("Ended {Span}. Start time: {Time} (as timestamp: {Timestamp})," + + _logger?.Trace()?.Log("Ended {Span}. Start time: {Time} (as timestamp: {Timestamp})," + " End time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, + this, formattedTimestamp, Timestamp, TimeUtils.FormatTimestampForLog(endTimestamp), endTimestamp, Duration); } @@ -473,7 +475,7 @@ public void End() } catch (Exception e) { - _logger.Warning()?.LogException(e, "Failed deducing destination fields for span."); + _logger?.Warning()?.LogException(e, "Failed deducing destination fields for span."); } if (_isDropped && _context.IsValueCreated) @@ -570,7 +572,7 @@ void QueueSpan(Span span) DroppedSpanStatCache.Value.DestinationServiceResource, _outcome, Duration!.Value); break; } - _logger.Trace()?.Log("Dropping fast exit span on composite span. Composite duration: {duration}", Composite.Sum); + _logger?.Trace()?.Log("Dropping fast exit span on composite span. Composite duration: {duration}", Composite.Sum); return; } if (span.Duration < span.Configuration.ExitSpanMinDuration) @@ -587,7 +589,7 @@ void QueueSpan(Span span) DroppedSpanStatCache.Value.DestinationServiceResource, _outcome, Duration!.Value); break; } - _logger.Trace()?.Log("Dropping fast exit span. Duration: {duration}", Duration); + _logger?.Trace()?.Log("Dropping fast exit span. Duration: {duration}", Duration); return; } } @@ -869,8 +871,7 @@ private Destination DeduceHttpDestination() } catch (Exception ex) { - _logger.Trace() - ?.LogException(ex, "Failed to deduce destination info from Context.Http." + _logger?.Trace()?.LogException(ex, "Failed to deduce destination info from Context.Http." + " Original URL: {OriginalUrl}. Context.Http.Url: {Context.Http.Url}." , Context.Http.OriginalUrl, Context.Http.Url); return null; diff --git a/src/Elastic.Apm/Model/SpanTimerKey.cs b/src/Elastic.Apm/Model/SpanTimerKey.cs index a881ceecf..2301fdc3b 100644 --- a/src/Elastic.Apm/Model/SpanTimerKey.cs +++ b/src/Elastic.Apm/Model/SpanTimerKey.cs @@ -8,12 +8,14 @@ namespace Elastic.Apm.Model /// /// Encapsulates type and subtype /// - internal struct SpanTimerKey + internal readonly record struct SpanTimerKey { public SpanTimerKey(string type, string subType) => (Type, SubType) = (type, subType); public SpanTimerKey(string type) => (Type, SubType) = (type, null); - public string Type { get; } - public string SubType { get; set; } - public static SpanTimerKey AppSpanType => new SpanTimerKey("app"); + + public readonly string Type { get; } + public readonly string SubType { get; } + + public static SpanTimerKey AppSpanType => new("app"); } } diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 9280a7809..3558d7a56 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -280,7 +280,7 @@ internal Transaction( } catch (Exception e) { - _logger.Error()?.LogException(e, "Error setting trace context on created activity"); + _logger?.Error()?.LogException(e, "Error setting trace context on created activity"); } } else @@ -327,22 +327,22 @@ internal Transaction( SpanCount = new SpanCount(); _currentExecutionSegmentsContainer.CurrentTransaction = this; + var formattedTimestamp = _logger.IsEnabled(LogLevel.Trace) ? TimeUtils.FormatTimestampForLog(Timestamp) : string.Empty; + if (isSamplingFromDistributedTracingData) { - _logger.Trace() - ?.Log("New Transaction instance created: {Transaction}. " + + _logger?.Trace()?.Log("New Transaction instance created: {Transaction}. " + "IsSampled ({IsSampled}) and SampleRate ({SampleRate}) is based on incoming distributed tracing data ({DistributedTracingData})." + " Start time: {Time} (as timestamp: {Timestamp})", - this, IsSampled, SampleRate, distributedTracingData, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp); + this, IsSampled, SampleRate, distributedTracingData, formattedTimestamp, Timestamp); } else { - _logger.Trace() - ?.Log("New Transaction instance created: {Transaction}. " + + _logger?.Trace()?.Log("New Transaction instance created: {Transaction}. " + "IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." + " Start time: {Time} (as timestamp: {Timestamp})", - this, IsSampled, sampler, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp); + this, IsSampled, sampler, formattedTimestamp, Timestamp); } } @@ -646,34 +646,38 @@ public string EnsureParentId() public void End() { + var endTimestamp = TimeUtils.TimestampNow(); + // If the outcome is still unknown and it was not specifically set to unknown, then it's success if (Outcome == Outcome.Unknown && !_outcomeChangedThroughApi) Outcome = Outcome.Success; + var formattedTimestamp = _logger.IsEnabled(LogLevel.Trace) ? TimeUtils.FormatTimestampForLog(Timestamp) : string.Empty; + if (Duration.HasValue) { - _logger.Trace() - ?.Log("Ended {Transaction} (with Duration already set)." + + _logger?.Trace()?.Log("Ended {Transaction} (with Duration already set)." + " Start time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, Duration); + this, formattedTimestamp, Timestamp, Duration); ChildDurationTimer.OnSpanEnd((long)(Timestamp + Duration.Value * 1000)); } else { - Assertion.IfEnabled?.That(!_isEnded, - $"Transaction's Duration doesn't have value even though {nameof(End)} method was already called." + - $" It contradicts the invariant enforced by {nameof(End)} method - Duration should have value when {nameof(End)} method exits" + - $" and {nameof(_isEnded)} field is set to true only when {nameof(End)} method exits." + - $" Context: this: {this}; {nameof(_isEnded)}: {_isEnded}"); + if (Assertion.IsEnabled && _isEnded) + { + throw new AssertionFailedException($"Transaction's Duration doesn't have value even though {nameof(End)} method was already called." + + $" It contradicts the invariant enforced by {nameof(End)} method - Duration should have value when {nameof(End)} method exits" + + $" and {nameof(_isEnded)} field is set to true only when {nameof(End)} method exits." + + $" Context: this: {this}; {nameof(_isEnded)}: {_isEnded}"); + } - var endTimestamp = TimeUtils.TimestampNow(); ChildDurationTimer.OnSpanEnd(endTimestamp); Duration = TimeUtils.DurationBetweenTimestamps(Timestamp, endTimestamp); - _logger.Trace() - ?.Log("Ended {Transaction}. Start time: {Time} (as timestamp: {Timestamp})," + + + _logger?.Trace()?.Log("Ended {Transaction}. Start time: {Time} (as timestamp: {Timestamp})," + " End time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, + this, formattedTimestamp, Timestamp, TimeUtils.FormatTimestampForLog(endTimestamp), endTimestamp, Duration); } @@ -699,8 +703,7 @@ public void End() { if (!CompressionBuffer.IsSampled && _apmServerInfo?.Version >= new ElasticVersion(8, 0, 0, string.Empty)) { - _logger?.Debug() - ?.Log("Dropping unsampled compressed span - unsampled span won't be sent on APM Server v8+. SpanId: {id}", + _logger?.Debug()?.Log("Dropping unsampled compressed span - unsampled span won't be sent on APM Server v8+. SpanId: {id}", CompressionBuffer.Id); } else @@ -734,8 +737,7 @@ public void End() } else { - _logger?.Debug() - ?.Log("Dropping unsampled transaction - unsampled transactions won't be sent on APM Server v8+. TransactionId: {id}", Id); + _logger?.Debug()?.Log("Dropping unsampled transaction - unsampled transactions won't be sent on APM Server v8+. TransactionId: {id}", Id); } _currentExecutionSegmentsContainer.CurrentTransaction = null; @@ -773,7 +775,7 @@ internal Span StartSpanInternal(string name, string type, string subType = null, { var retVal = new Span(name, type, Id, TraceId, this, _sender, _logger, _currentExecutionSegmentsContainer, _apmServerInfo, instrumentationFlag: instrumentationFlag, captureStackTraceOnStart: captureStackTraceOnStart, timestamp: timestamp, - isExitSpan: isExitSpan, id: id, links: links, current: current); + isExitSpan: isExitSpan, id: id, links: links); ChildDurationTimer.OnChildStart(retVal.Timestamp); if (!string.IsNullOrEmpty(subType)) @@ -782,7 +784,7 @@ internal Span StartSpanInternal(string name, string type, string subType = null, if (!string.IsNullOrEmpty(action)) retVal.Action = action; - _logger.Trace()?.Log("Starting {SpanDetails}", retVal.ToString()); + _logger?.Trace()?.Log("Starting {SpanDetails}", retVal.ToString()); return retVal; } diff --git a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs index dca9db9c7..a7678f7e7 100644 --- a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs +++ b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs @@ -3,10 +3,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -22,15 +21,15 @@ namespace Elastic.Apm.OpenTelemetry { public class ElasticActivityListener : IDisposable { - private static readonly string[] ServerPortAttributeKeys = new[] { SemanticConventions.ServerPort, SemanticConventions.NetPeerPort }; + private static readonly string[] ServerPortAttributeKeys = [SemanticConventions.ServerPort, SemanticConventions.NetPeerPort]; private static readonly string[] ServerAddressAttributeKeys = - new[] { SemanticConventions.ServerAddress, SemanticConventions.NetPeerName, SemanticConventions.NetPeerIp }; + [SemanticConventions.ServerAddress, SemanticConventions.NetPeerName, SemanticConventions.NetPeerIp]; private static readonly string[] HttpAttributeKeys = - new[] { SemanticConventions.UrlFull, SemanticConventions.HttpUrl, SemanticConventions.HttpScheme }; + [SemanticConventions.UrlFull, SemanticConventions.HttpUrl, SemanticConventions.HttpScheme]; - private static readonly string[] HttpUrlAttributeKeys = new[] { SemanticConventions.UrlFull, SemanticConventions.HttpUrl }; + private static readonly string[] HttpUrlAttributeKeys = [SemanticConventions.UrlFull, SemanticConventions.HttpUrl]; private readonly ConditionalWeakTable _activeSpans = new(); private readonly ConditionalWeakTable _activeTransactions = new(); @@ -77,7 +76,7 @@ internal void Start(Tracer tracerInternal) if (HasServiceBusInstrumentation && activity.Tags.Any(kvp => kvp.Key.Equals("az.namespace", StringComparison.Ordinal) && kvp.Value.Equals("Microsoft.ServiceBus", StringComparison.Ordinal))) { - _logger.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.ServiceBus' " + + _logger?.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.ServiceBus' " + "activity because 'Elastic.Apm.Azure.ServiceBus' is present in the application.", activity.DisplayName, activity.Id, activity.TraceId); @@ -87,7 +86,7 @@ internal void Start(Tracer tracerInternal) if (HasStorageInstrumentation && activity.Tags.Any(kvp => kvp.Key.Equals("az.namespace", StringComparison.Ordinal) && kvp.Value.Equals("Microsoft.Storage", StringComparison.Ordinal))) { - _logger.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.Storage' " + + _logger?.Debug()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped 'Microsoft.Storage' " + "activity because 'Elastic.Apm.Azure.Storage' is present in the application.", activity.DisplayName, activity.Id, activity.TraceId); @@ -96,12 +95,12 @@ internal void Start(Tracer tracerInternal) if (KnownListeners.SkippedActivityNamesSet.Contains(activity.DisplayName)) { - _logger.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped because it matched " + - "a skipped activity name defined in KnownListeners."); + _logger?.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId} skipped because it matched " + + "a skipped activity name defined in KnownListeners.", activity.DisplayName, activity.Id, activity.TraceId); return; } - _logger.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId}", + _logger?.Trace()?.Log("ActivityStarted: name:{DisplayName} id:{ActivityId} traceId:{TraceId}", activity.DisplayName, activity.Id, activity.TraceId); var spanLinks = new List(activity.Links.Count()); @@ -176,12 +175,12 @@ private void CreateSpanForActivity(Activity activity, long timestamp, List Date: Fri, 29 Nov 2024 11:26:30 +0000 Subject: [PATCH 55/77] Cleanup unused files in .ci directory (#2494) Closes #2490 Note that some files are still used for synching the spec files so those have been left in place. --- .ci/ci.csproj | 12 ----------- .ci/{linux => }/deploy.sh | 0 .ci/docker/iis/Dockerfile | 33 ------------------------------- .ci/docker/iis/README.md | 17 ---------------- .ci/docker/iis/content/index.html | 10 ---------- .ci/packer_cache.sh | 5 ----- .github/workflows/release.yml | 2 +- ElasticApmAgent.sln | 4 ---- global.json | 2 +- 9 files changed, 2 insertions(+), 83 deletions(-) delete mode 100644 .ci/ci.csproj rename .ci/{linux => }/deploy.sh (100%) mode change 100755 => 100644 delete mode 100644 .ci/docker/iis/Dockerfile delete mode 100644 .ci/docker/iis/README.md delete mode 100644 .ci/docker/iis/content/index.html delete mode 100755 .ci/packer_cache.sh diff --git a/.ci/ci.csproj b/.ci/ci.csproj deleted file mode 100644 index 0da2792d6..000000000 --- a/.ci/ci.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netstandard2.0 - false - false - true - false - false - $(ProjectDir) - - diff --git a/.ci/linux/deploy.sh b/.ci/deploy.sh old mode 100755 new mode 100644 similarity index 100% rename from .ci/linux/deploy.sh rename to .ci/deploy.sh diff --git a/.ci/docker/iis/Dockerfile b/.ci/docker/iis/Dockerfile deleted file mode 100644 index 7a16a993d..000000000 --- a/.ci/docker/iis/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM mcr.microsoft.com/dotnet/framework/aspnet - -RUN powershell -NoProfile -Command Remove-Item -Recurse C:\\inetpub\wwwroot\* - -SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] - -RUN New-Item -Path "c:\\" -Name "tools" -ItemType "directory" - -WORKDIR /tools - -RUN "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12" - -# Install chocolatey -RUN iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) - -# Install .NET (Core) SDKs -RUN Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile dotnet-install.ps1 -UseBasicParsing -RUN & ./dotnet-install.ps1 -Channel LTS -InstallDir ./dotnet -Version 5.0.100 - -# Install NuGet Tool -RUN Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile dotnet\\nuget.exe -UseBasicParsing - -# Install 2019 build tools -RUN Invoke-WebRequest -UseBasicParsing "https://download.visualstudio.microsoft.com/download/pr/cb1d5164-e767-4886-8955-2df3a7c816a8/b9ff67da6d68d6a653a612fd401283cc213b4ec4bae349dd3d9199659a7d9354/vs_BuildTools.exe" -OutFile "C:\tools\vs_BuildTools.exe"; \ - Start-Process vs_BuildTools.exe -ArgumentList "--add", "Microsoft.VisualStudio.Component.NuGet", \ - "--add", "'Microsoft.VisualStudio.Workload.NetCoreBuildTools;includeRecommended;includeOptional'", \ - "--add", "Microsoft.VisualStudio.Workload.MSBuildTools", \ - "--add", "'Microsoft.VisualStudio.Workload.WebBuildTools;includeRecommended;includeOptional'", \ - "--quiet", "--norestart", "--nocache" -NoNewWindow -Wait; - -ENV PATH="C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Windows\\System32\\OpenSSH;C:\\Users\\ContainerAdministrator\\AppData\\Local\\Microsoft\\WindowsApps;c:\\tools;c:\\tools\\dotnet;C:\\ProgramData\\chocolatey\\bin" - - diff --git a/.ci/docker/iis/README.md b/.ci/docker/iis/README.md deleted file mode 100644 index ae50c6b4e..000000000 --- a/.ci/docker/iis/README.md +++ /dev/null @@ -1,17 +0,0 @@ -This is a Docker container with VisualStudio 2017, MSBuild tools, -dotnet SDK and an IIS installed. -This container could be used to build and run the IIS test. -The following commands would build the image and run the tests. - ----- - -**IMPORTANT**: This uses a Windows container, so can only be run with Docker on Windows, switched to use Windows containers - ----- - -``` -docker build -t iis .ci\docker\iis -docker run --name iis -v "${PWD}:C:\inetpub\wwwroot" -w C:\inetpub\wwwroot\ iis -docker exec iis cmd /C .ci\windows\msbuild.bat -docker exec iis cmd /C .ci\windows\test-iis.bat -``` \ No newline at end of file diff --git a/.ci/docker/iis/content/index.html b/.ci/docker/iis/content/index.html deleted file mode 100644 index 5960eee56..000000000 --- a/.ci/docker/iis/content/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - Hello Docker Windows - - \ No newline at end of file diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh deleted file mode 100755 index f57471210..000000000 --- a/.ci/packer_cache.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC1091 -source /usr/local/bin/bash_standard_lib.sh - -ARCH=$(uname -m| tr '[:upper:]' '[:lower:]') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef1f70410..b3d4b7548 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: run: ./build.sh pack - name: Release to Nuget - run: .ci/linux/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} + run: .ci/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 56ed373a1..a0f8b6997 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -45,8 +45,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.AspNetFullFrame EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.Tests.MockApmServer", "test\Elastic.Apm.Tests.MockApmServer\Elastic.Apm.Tests.MockApmServer.csproj", "{AFFB238B-10C0-4229-9527-184EFCF0F5AC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ci", ".ci\ci.csproj", "{9AAC1E7A-5887-46BC-96DA-DDFFF0F7CDC2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.EntityFrameworkCore.Tests", "test\instrumentations\Elastic.Apm.EntityFrameworkCore.Tests\Elastic.Apm.EntityFrameworkCore.Tests.csproj", "{79BEA612-E2C8-47FD-84DC-A1B17AAD5038}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.SqlClient.Tests", "test\instrumentations\Elastic.Apm.SqlClient.Tests\Elastic.Apm.SqlClient.Tests.csproj", "{968E1E85-E996-42DE-9845-D20DAE16165A}" @@ -300,8 +298,6 @@ Global {AFFB238B-10C0-4229-9527-184EFCF0F5AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFFB238B-10C0-4229-9527-184EFCF0F5AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {AFFB238B-10C0-4229-9527-184EFCF0F5AC}.Release|Any CPU.Build.0 = Release|Any CPU - {9AAC1E7A-5887-46BC-96DA-DDFFF0F7CDC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AAC1E7A-5887-46BC-96DA-DDFFF0F7CDC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {79BEA612-E2C8-47FD-84DC-A1B17AAD5038}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79BEA612-E2C8-47FD-84DC-A1B17AAD5038}.Debug|Any CPU.Build.0 = Debug|Any CPU {79BEA612-E2C8-47FD-84DC-A1B17AAD5038}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/global.json b/global.json index 789bff3bd..42f2b9487 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "8.0.404", "rollForward": "latestFeature", "allowPrerelease": false } From 8edcb491410e3f72f8a48e2d21c5a3a406af2c08 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 29 Nov 2024 14:28:34 +0000 Subject: [PATCH 56/77] Fixes and enhancements for Azure Functions (isolated worker) (#2505) This PR fixes several things in Azure Functions (isolated worker) which occur when using the latest templates for Azure Functions. - Newer versions of the Functions library prefer the gRPC-based implementation, which throws when `Url` is accessed. Instead, we read these from other data on the `FunctionContext`. - Address Functions library changes that break distributed tracing. We now parse the original headers from the `BindingContext` instead of the request, which may contain a `traceparent` with the sampling flag set to false when the user request does not include a specific `traceparent`. - We explicitly don't record activities from the Azure functions library as these are pretty broken (https://github.com/Azure/azure-functions-dotnet-worker/issues/2733) and are redundant when using our middleware. - Downgrade several packages as the newer ones are now deprecated (thanks for the confusion, Microsoft!). - Update some outdated compiler pre-processor directives. - The final few commits focus on CI integration test hangs on Linux. We don't have a perfect solution for those, but after reviewing the hang dumps, I've avoided some of the potential causes of the hangs. We'll monitor subsequent PRs, and if they remain stable, we will readdress the original causes. A follow-up PR will update our documentation. Closes #2407 Closes #2311 Closes #2218 --- .github/workflows/test-linux.yml | 11 ++- .github/workflows/test-windows.yml | 8 +- Directory.Packages.props | 19 ++-- src/Elastic.Apm/AgentComponents.cs | 14 +-- .../Cloud/AzureFunctionsMetadataProvider.cs | 11 ++- .../OpenTelemetry/ElasticActivityListener.cs | 11 +++ src/Elastic.Apm/Report/PayloadSenderV2.cs | 2 +- .../ApmMiddleware.cs | 89 ++++++++++++++++--- .../Elastic.Apm.Azure.Functions.csproj | 13 ++- .../Elastic.Apm.AspNetCore.csproj | 4 +- .../Elastic.Apm.Extensions.Hosting.csproj | 8 +- .../XUnit/DockerAttributes.cs | 2 +- .../Elastic.AzureFunctionApp.Isolated.csproj | 6 +- .../HttpTrigger.cs | 10 +-- .../Program.cs | 2 +- .../host.json | 11 ++- .../ExtensionsTestHelpers.cs | 6 ++ 17 files changed, 171 insertions(+), 56 deletions(-) diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 338bad2f9..e0b0291af 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -126,11 +126,14 @@ jobs: - name: Store crash dumps uses: actions/upload-artifact@v4 - if: success() || failure() + if: failure() with: - name: results - retention-days: 1 - path: build/output/**/*.dmp + name: hang-dumps + retention-days: 3 + path: | + build/output/**/*.dmp + build/output/**/*.xml + build/output/**/*.pdb startup-hook-tests: runs-on: ubuntu-latest diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 96ac00e54..145e86bbf 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -76,9 +76,11 @@ jobs: uses: actions/upload-artifact@v4 if: success() || failure() with: - name: results - retention-days: 1 - path: build/output/**/*.dmp + name: hang-dumps + retention-days: 3 + path: | + build/output/**/*.dmp + build/output/**/*.xml startup-hook-tests: runs-on: windows-2022 diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e37e4d0c..86864e2b8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,10 +84,10 @@ - - - - + + + + @@ -95,11 +95,12 @@ - - - - - + + + + + + diff --git a/src/Elastic.Apm/AgentComponents.cs b/src/Elastic.Apm/AgentComponents.cs index 82898f6e7..f8b815d1f 100644 --- a/src/Elastic.Apm/AgentComponents.cs +++ b/src/Elastic.Apm/AgentComponents.cs @@ -17,7 +17,7 @@ using Elastic.Apm.Metrics.MetricsProvider; using Elastic.Apm.Report; using Elastic.Apm.ServerInfo; -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER using Elastic.Apm.OpenTelemetry; #endif @@ -60,7 +60,7 @@ internal AgentComponents( ApmServerInfo = apmServerInfo ?? new ApmServerInfo(); HttpTraceConfiguration = new HttpTraceConfiguration(); -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER // Initialize early because ServerInfoCallback requires it and might execute // before EnsureElasticActivityStarted runs ElasticActivityListener = new ElasticActivityListener(this, HttpTraceConfiguration); @@ -81,7 +81,7 @@ internal AgentComponents( currentExecutionSegmentsContainer ?? new CurrentExecutionSegmentsContainer(), ApmServerInfo, breakdownMetricsProvider); -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER EnsureElasticActivityStarted(); #endif @@ -118,7 +118,7 @@ internal AgentComponents( private void EnsureElasticActivityStarted() { -#if !NET5_0_OR_GREATER +#if !NET8_0_OR_GREATER return; #else if (!Configuration.OpenTelemetryBridgeEnabled) return; @@ -145,7 +145,7 @@ private void EnsureElasticActivityStarted() private void ServerInfoCallback(bool success, IApmServerInfo serverInfo) { -#if !NET5_0_OR_GREATER +#if !NET8_0_OR_GREATER return; #else if (!Configuration.OpenTelemetryBridgeEnabled) return; @@ -237,7 +237,7 @@ internal static IApmLogger GetGlobalLogger(IApmLogger fallbackLogger, LogLevel a return fallbackLogger; } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER private ElasticActivityListener ElasticActivityListener { get; } #endif @@ -281,7 +281,7 @@ public void Dispose() if (PayloadSender is IDisposable disposablePayloadSender) disposablePayloadSender.Dispose(); CentralConfigurationFetcher?.Dispose(); -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER ElasticActivityListener?.Dispose(); #endif } diff --git a/src/Elastic.Apm/Cloud/AzureFunctionsMetadataProvider.cs b/src/Elastic.Apm/Cloud/AzureFunctionsMetadataProvider.cs index ad02e58c3..4e53bde2a 100644 --- a/src/Elastic.Apm/Cloud/AzureFunctionsMetadataProvider.cs +++ b/src/Elastic.Apm/Cloud/AzureFunctionsMetadataProvider.cs @@ -59,7 +59,16 @@ internal static AzureFunctionsMetaData GetAzureFunctionsMetaData(IApmLogger logg functionsExtensionVersion) || helper.NullOrEmptyVariable(AzureEnvironmentVariables.WebsiteOwnerName, websiteOwnerName) || helper.NullOrEmptyVariable(AzureEnvironmentVariables.WebsiteSiteName, websiteSiteName)) - return new AzureFunctionsMetaData { IsValid = false }; + return new AzureFunctionsMetaData + { + IsValid = false, + RegionName = regionName, + FunctionsExtensionVersion = functionsExtensionVersion, + FunctionsWorkerRuntime = functionsWorkerRuntime, + WebsiteSiteName = websiteSiteName, + WebsiteResourceGroup = websiteResourceGroup, + WebsiteInstanceId = websiteInstanceId, + }; var tokens = helper.TokenizeWebSiteOwnerName(websiteOwnerName); if (!tokens.HasValue) diff --git a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs index a7678f7e7..89fe3e57d 100644 --- a/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs +++ b/src/Elastic.Apm/OpenTelemetry/ElasticActivityListener.cs @@ -71,6 +71,17 @@ internal void Start(Tracer tracerInternal) private Action ActivityStarted => activity => { + // Prevent recording of Azure Functions activities which are quite broken at the moment + // See https://github.com/Azure/azure-functions-dotnet-worker/issues/2733 + // See https://github.com/Azure/azure-functions-dotnet-worker/issues/2875 + // See https://github.com/Azure/azure-functions-host/issues/10641 + // See https://github.com/Azure/azure-functions-dotnet-worker/issues/2810 + if ((activity.Source.Name == "" && activity.DisplayName == "InvokeFunctionAsync") + || (activity.Source.Name == "Microsoft.Azure.Functions.Worker")) + { + return; + } + // If the Elastic instrumentation for ServiceBus is present, we skip duplicating the instrumentation through the OTel bridge. // Without this, we end up with some redundant spans in the trace with subtle differences. if (HasServiceBusInstrumentation && activity.Tags.Any(kvp => diff --git a/src/Elastic.Apm/Report/PayloadSenderV2.cs b/src/Elastic.Apm/Report/PayloadSenderV2.cs index f09f9d5e8..2e85fa893 100644 --- a/src/Elastic.Apm/Report/PayloadSenderV2.cs +++ b/src/Elastic.Apm/Report/PayloadSenderV2.cs @@ -398,7 +398,7 @@ private void ProcessQueueItems(object[] queueItems) { content.Headers.ContentType = MediaTypeHeaderValue; -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER HttpResponseMessage response; try { diff --git a/src/azure/Elastic.Apm.Azure.Functions/ApmMiddleware.cs b/src/azure/Elastic.Apm.Azure.Functions/ApmMiddleware.cs index 021b0c76c..83a94ee7e 100644 --- a/src/azure/Elastic.Apm.Azure.Functions/ApmMiddleware.cs +++ b/src/azure/Elastic.Apm.Azure.Functions/ApmMiddleware.cs @@ -10,6 +10,8 @@ using Elastic.Apm.Extensions; using Elastic.Apm.Logging; using Elastic.Apm.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker.Middleware; @@ -56,24 +58,88 @@ await Agent.Tracer.CaptureTransaction(data.Name, ApiConstants.TypeRequest, async }, data.TracingData); } + private const string TraceParentHeader = "\"traceparent\":"; + private const string TraceStateHeader = "\"tracestate\":"; + private static TriggerSpecificData GetTriggerSpecificData(FunctionContext context) { - var httpRequestData = GetHttpRequestData(context); - if (httpRequestData != null) // HTTP Trigger + try { - Context.Logger.Trace()?.Log("HTTP Trigger type detected."); + string? traceparent = null; + string? tracestate = null; + + // NOTE: We parse the original headers from BindingData as internally the Host sends the GrpcWorker a request. When no traceparent + // is present on the original request to the Functions host, one is added, with the sampled flag set as false. This is then present + // if we try to read it from DefaultHttpContext which means we don't record transactions. Currently this works around that issue to + // ensure we capture the trace based on the original request to the Functions host. + if (context.BindingContext.BindingData.TryGetValue("Headers", out var headers) && headers is string headersAsString) + { + var span = headersAsString.AsSpan(); + var indexOfTraceparent = span.IndexOf(TraceParentHeader.AsSpan(), StringComparison.OrdinalIgnoreCase); + + if (indexOfTraceparent > -1) + { + var traceparentSpan = span.Slice(indexOfTraceparent + TraceParentHeader.Length + 1); + var endOfTraceparentSpan = traceparentSpan.IndexOf('"'); + traceparentSpan = traceparentSpan.Slice(0, endOfTraceparentSpan); + + if (traceparentSpan.Length == 55) + traceparent = traceparentSpan.ToString(); + } - httpRequestData.Headers.TryGetValues("traceparent", out var traceparent); - httpRequestData.Headers.TryGetValues("tracestate", out var tracestate); + var indexOfTracestate = span.IndexOf(TraceStateHeader.AsSpan(), StringComparison.OrdinalIgnoreCase); - return new($"{httpRequestData.Method} {httpRequestData.Url.AbsolutePath}", Trigger.TypeHttp, - TraceContext.TryExtractTracingData(traceparent?.FirstOrDefault(), tracestate?.FirstOrDefault())) + if (indexOfTracestate > -1) + { + var tracestateSpan = span.Slice(indexOfTracestate + TraceStateHeader.Length + 1); + var endOfTracestateSpan = tracestateSpan.IndexOf('"'); + tracestate = tracestateSpan.Slice(0, endOfTracestateSpan).ToString(); + } + } + + var httpRequestContext = context.Items + .Where(i => i.Key is string s && s.Equals("HttpRequestContext", StringComparison.Ordinal)) + .Select(i => i.Value) + .SingleOrDefault(); + + if (httpRequestContext is DefaultHttpContext requestContext) { - Request = new Request(httpRequestData.Method, Url.FromUri(httpRequestData.Url)) + Context.Logger.Trace()?.Log("HTTP Trigger type detected."); + + var httpRequest = requestContext.Request; + + if (Uri.TryCreate(httpRequest.GetDisplayUrl(), UriKind.Absolute, out var uri)) { - Headers = CreateHeadersDictionary(httpRequestData.Headers), + return new($"{httpRequest.Method} {uri.AbsolutePath}", Trigger.TypeHttp, + TraceContext.TryExtractTracingData(traceparent, tracestate)) + { + Request = new Request(httpRequest.Method, Url.FromUri(uri)) + { + Headers = CreateHeadersDictionary(httpRequest.Headers), + } + }; } - }; + } + + // This is the original code, left as a fallback for older versions of the Functions library + var httpRequestData = GetHttpRequestData(context); + if (httpRequestData != null) // HTTP Trigger + { + Context.Logger.Trace()?.Log("HTTP Trigger type detected."); + + return new($"{httpRequestData.Method} {httpRequestData.Url.AbsolutePath}", Trigger.TypeHttp, + TraceContext.TryExtractTracingData(traceparent, tracestate)) + { + Request = new Request(httpRequestData.Method, Url.FromUri(httpRequestData.Url)) + { + Headers = CreateHeadersDictionary(httpRequestData.Headers), + } + }; + } + } + catch + { + // ignored } // Generic @@ -114,6 +180,9 @@ private static void SetTriggerSpecificResult(ITransaction transaction, bool succ private static Dictionary CreateHeadersDictionary(HttpHeadersCollection httpHeadersCollection) => httpHeadersCollection.ToDictionary(h => h.Key, h => string.Join(",", h.Value)); + private static Dictionary CreateHeadersDictionary(IHeaderDictionary headerDictionary) => + headerDictionary.ToDictionary(h => h.Key, h => string.Join(",", h.Value.AsEnumerable())); + private static HttpRequestData? GetHttpRequestData(FunctionContext functionContext) { var inputData = GetProperty>(functionContext, "InputData"); diff --git a/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj b/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj index d00bf90ba..d8c285e04 100644 --- a/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj +++ b/src/azure/Elastic.Apm.Azure.Functions/Elastic.Apm.Azure.Functions.csproj @@ -1,9 +1,8 @@ - netstandard2.0 + netstandard2.0;net8.0 enable - Elastic.Apm.Azure.Functions Elastic.Apm.Azure.Functions Elastic.Apm.Azure.Functions @@ -18,9 +17,17 @@ - + + + + + + + + + diff --git a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj index 54566d7eb..c12b97002 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj +++ b/src/integrations/Elastic.Apm.AspNetCore/Elastic.Apm.AspNetCore.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj index 0a148d83f..e6395ec09 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj @@ -21,10 +21,10 @@ - - - - + + + + diff --git a/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs b/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs index b47eb257e..8332ea684 100644 --- a/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs +++ b/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs @@ -67,7 +67,7 @@ static DockerUtils() { try { - var result = Proc.Start(new StartArguments("docker", "--version")); + var result = Proc.Start(new StartArguments("docker", "--version"), TimeSpan.FromSeconds(30)); HasDockerInstalled = result.ExitCode == 0; } catch (Exception) diff --git a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj index 0523ea9ab..82af8f6be 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj +++ b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Elastic.AzureFunctionApp.Isolated.csproj @@ -8,13 +8,11 @@ false - + + diff --git a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/HttpTrigger.cs b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/HttpTrigger.cs index 09be28b24..e9022ed43 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/HttpTrigger.cs +++ b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/HttpTrigger.cs @@ -14,7 +14,7 @@ namespace Elastic.AzureFunctionApp.Isolated; public static class HttpTriggers { [Function(FunctionName.SampleHttpTrigger)] - public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, + public static async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, FunctionContext executionContext) { var logger = executionContext.GetLogger("SampleHttpTrigger"); @@ -23,11 +23,11 @@ public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "ge var response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); - response.WriteString("Hello Azure Functions!\n"); - response.WriteString("======================\n"); + await response.WriteStringAsync("Hello Azure Functions!\n"); + await response.WriteStringAsync("======================\n"); foreach (DictionaryEntry e in Environment.GetEnvironmentVariables()) - response.WriteString($"{e.Key} = {e.Value}\n"); - response.WriteString("======================\n"); + await response.WriteStringAsync($"{e.Key} = {e.Value}\n"); + await response.WriteStringAsync("======================\n"); return response; } diff --git a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Program.cs b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Program.cs index cd8133c6c..f888a1f57 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Program.cs +++ b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/Program.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting; var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults(builder => + .ConfigureFunctionsWebApplication(builder => { builder.UseMiddleware(); }) diff --git a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/host.json b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/host.json index b9f92c0de..0414a41c6 100644 --- a/test/azure/applications/Elastic.AzureFunctionApp.Isolated/host.json +++ b/test/azure/applications/Elastic.AzureFunctionApp.Isolated/host.json @@ -1,3 +1,12 @@ { - "version": "2.0" + "version": "2.0", + "logging": { + "logLevel": { + "default": "Debug" + }, + "console": { + "isEnabled": true, + "DisableColors": true + } + } } \ No newline at end of file diff --git a/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs index e8c8459c8..465ea5ec5 100644 --- a/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs +++ b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using System.Runtime.InteropServices; using Elastic.Apm.Tests.Utilities; using FluentAssertions; using Xunit.Abstractions; @@ -58,6 +59,11 @@ public async Task ExecuteTestProcessAsync(bool? enabled, bool registerTwice, boo if (disabledViaEnvVar) startInfo.Environment["ELASTIC_APM_ENABLED"] = "false"; + // Adding this as the hang dumps for integration tests on Linux seem to suggest that the GcMetricsProvider is causing the hang. + // This is a temporary workaround until we can figure out the root cause. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + startInfo.Environment["ELASTIC_APM_METRICS_INTERVAL"] = "0"; + startInfo.ArgumentList.Add("run"); startInfo.ArgumentList.Add("-c"); startInfo.ArgumentList.Add("Release"); From 7dd5b113d55f1e82e474368f174f3466d358e9bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:01:39 +0000 Subject: [PATCH 57/77] Bump docker/metadata-action from 5.5.1 to 5.6.1 in the github-actions group (#2497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 1 update: [docker/metadata-action](https://github.com/docker/metadata-action). Updates `docker/metadata-action` from 5.5.1 to 5.6.1
Release notes

Sourced from docker/metadata-action's releases.

v5.6.1

Full Changelog: https://github.com/docker/metadata-action/compare/v5.6.0...v5.6.1

v5.6.0

Full Changelog: https://github.com/docker/metadata-action/compare/v5.5.1...v5.6.0

Commits
  • 369eb59 Merge pull request #480 from crazy-max/back-to-sha-7
  • 7d870ce chore: update generated content
  • e44a9cd back to commit sha length of 7
  • 8cb0002 Merge pull request #478 from crazy-max/commit-date-request
  • e01ddd3 chore: update generated content
  • 861d98a commiter_date: fix github api request fallback
  • 359e915 Merge pull request #475 from crazy-max/commit-date-changes
  • 0c395eb commit_date: code cleanup and readme updates
  • 1156622 Merge pull request #474 from docker/dependabot/npm_and_yarn/cross-spawn-7.0.5
  • 95ea8d0 chore(deps): Bump cross-spawn from 7.0.3 to 7.0.5
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/metadata-action&package-manager=github_actions&previous-version=5.5.1&new-version=5.6.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index b921551bd..d9f3f64b8 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -58,7 +58,7 @@ jobs: - name: Extract metadata (tags, labels) id: docker-meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 with: images: ${{ env.DOCKER_IMAGE_NAME }} flavor: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3d4b7548..6773bcb4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: - name: Extract metadata (tags, labels) id: docker-meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 with: images: ${{ env.DOCKER_IMAGE_NAME }} flavor: | From ed91a91805d1b4fae6a3dce2e80185729038e6ff Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 29 Nov 2024 15:58:26 +0000 Subject: [PATCH 58/77] Update Azure Function service name logic (#2508) As titled. --- .../Elastic.Apm.Azure.Functions/AzureFunctionsContext.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/azure/Elastic.Apm.Azure.Functions/AzureFunctionsContext.cs b/src/azure/Elastic.Apm.Azure.Functions/AzureFunctionsContext.cs index 9bbdfe731..1aa573a1b 100644 --- a/src/azure/Elastic.Apm.Azure.Functions/AzureFunctionsContext.cs +++ b/src/azure/Elastic.Apm.Azure.Functions/AzureFunctionsContext.cs @@ -40,11 +40,15 @@ private void UpdateServiceInformation(Service? service) return; } - if (service.Name == AbstractConfigurationReader.AdaptServiceName(AbstractConfigurationReader.DiscoverDefaultServiceName())) + var defaultServiceName = AbstractConfigurationReader.AdaptServiceName(AbstractConfigurationReader.DiscoverDefaultServiceName()); + + // In local development `WEBSITE_SITE_NAME` may not be set, in which case, we use the discovered default service name + if (service.Name == defaultServiceName && MetaData.WebsiteSiteName is not null) { // Only override the service name if it was set to default. service.Name = MetaData.WebsiteSiteName; } + service.Framework = new() { Name = "Azure Functions", Version = MetaData.FunctionsExtensionVersion }; var runtimeVersion = service.Runtime?.Version ?? "n/a"; service.Runtime = new() { Name = MetaData.FunctionsWorkerRuntime, Version = runtimeVersion }; From 9c0b926f0ce0a3739ec1d7537b798a3fafef1125 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 29 Nov 2024 15:58:47 +0000 Subject: [PATCH 59/77] Update Azure Functions documentation (#2507) As titled. --- docs/images/azure-functions-configuration.png | Bin 272462 -> 53669 bytes docs/setup-azure-functions.asciidoc | 75 +++++++++++++----- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/docs/images/azure-functions-configuration.png b/docs/images/azure-functions-configuration.png index 5303f64e46b9a28e53d58e02979de8a32d8efeb4..751b51870f4de36bdccee975650599d5ed06ba7e 100644 GIT binary patch literal 53669 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�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
  • })x9{H%0Klt**z*3c@e$&Aoah)p9Opj%5eCF+ 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%0DSY|uTRRS^Gi48Je#gH)@=q^|FP;Q~ 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

    AA9&Sqif% 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 C#iDHh diff --git a/docs/setup-azure-functions.asciidoc b/docs/setup-azure-functions.asciidoc index adb432510..ff1ce1d45 100644 --- a/docs/setup-azure-functions.asciidoc +++ b/docs/setup-azure-functions.asciidoc @@ -2,7 +2,8 @@ [[setup-azure-functions]] === Azure Functions -The .NET APM Agent can trace function invocations in an https://learn.microsoft.com/en-us/azure/azure-functions[Azure Functions] app. +The .NET APM Agent can trace HTTP triggered function invocations in an +https://learn.microsoft.com/en-us/azure/azure-functions[Azure Functions] app. [float] ==== Prerequisites @@ -17,18 +18,14 @@ existing one, you can follow https://learn.microsoft.com/en-us/azure/azure-funct to create one. You can also take a look at and use this -https://github.com/elastic/apm-agent-dotnet/tree/main/sample/Elastic.AzureFunctionApp.Isolated[Azure Functions example app with Elastic APM already integrated]. +https://github.com/elastic/apm-agent-dotnet/tree/main/test/azure/applications/Elastic.AzureFunctionApp.Isolated[Azure Functions example app with Elastic APM already integrated]. -[IMPORTANT] -==== -Currently, only .NET Azure Functions in an -https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide[isolated worker process] -can be traced. -==== +[float] +==== Azure Functions isolated worker model [float] [[azure-functions-setup]] -==== Step 1: Add the NuGet package +===== Step 1: Add the NuGet package Add the `Elastic.Apm.Azure.Functions` NuGet package to your Azure Functions project: @@ -38,7 +35,7 @@ dotnet add package Elastic.Apm.Azure.Functions ---- [float] -==== Step 2: Add the tracing Middleware +===== Step 2: Add the tracing Middleware For the APM agent to trace Azure Functions invocations, the `Elastic.Apm.Azure.Functions.ApmMiddleware` must be used in your Azure Functions app. @@ -49,31 +46,65 @@ using Elastic.Apm.Azure.Functions; using Microsoft.Extensions.Hosting; var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults(builder => - { - builder.UseMiddleware(); - }) - .Build(); + .ConfigureFunctionsWebApplication(builder => + { + builder.UseMiddleware(); + }) + .Build(); host.Run(); ---- [float] -==== Step 3: Configure the APM agent - -The APM agent can be configured with environment variables. Using environment variables -allows you to use https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings[application settings in the Azure Portal], enabling you to hide values and update settings -without needing to re-deploy code. +===== Step 3: Configure the APM agent -Open _Configuration > Application settings_ for your Function App in the Azure Portal -and set: +The APM agent can be configured with environment variables. [source,yaml] ---- ELASTIC_APM_SERVER_URL: ELASTIC_APM_SECRET_TOKEN: +ELASTIC_APM_ENVIRONMENT: +ELASTIC_APM_SERVICE_NAME: (optional) +---- + +If `ELASTIC_APM_SERVICE_NAME` is not configured, the agent will use a fallback value. + +- *Local development* - The discovered service name (the entry Assembly name) will be used. +- *Azure* - The Function App name (retrieved from the `WEBSITE_SITE_NAME` environment variable) will be used. + +*Configuring in Local development* + +While developing your Function locally, you can configure the agent by providing the environment variables +via the `local.settings.json` file. + +For example: + +[source,json] +---- +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "ELASTIC_APM_ENVIRONMENT": "Development", + "ELASTIC_APM_SERVICE_NAME": "MyServiceName", + "ELASTIC_APM_SERVER_URL": "https://my-serverless-project.apm.eu-west-1.aws.elastic.cloud:443", + "ELASTIC_APM_API_KEY": "MySecureApiKeyFromApmServer==" + } +} ---- +*Configuring in Azure* + +Using environment variables allows you to use +https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings[application settings in the Azure Portal], +enabling you to update settings +without needing to re-deploy code. + +Open _Settings > Environment variables_ for your Function App in the Azure Portal +and configure the ELASTIC_APM_* variables as required. + For example: image::./images/azure-functions-configuration.png[Configuring the APM Agent in the Azure Portal] From 4cc5cd80bd112390177101ca1e478a752cb4c1ce Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 2 Dec 2024 11:03:47 +0100 Subject: [PATCH 60/77] github-actions: dependabot cannot use the id-token:write (#2510) --- .github/workflows/test-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index e0b0291af..e5d20411f 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -79,7 +79,7 @@ jobs: needs: [ 'format', 'tests' ] if: | github.event_name != 'pull_request' - || github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + || github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]' env: ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} From 6d7ccea5ba921c354bcbe45c365c4f60a9fb26ca Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 2 Dec 2024 11:52:22 +0100 Subject: [PATCH 61/77] rename and add updatecli policies autodiscovery (#2511) --- .ci/updatecli/values.d/scm.yml | 7 +++++-- .ci/updatecli/values.d/update-compose.yml | 3 +++ .github/workflows/updatecli.yml | 6 ++++++ update-compose.yaml | 14 +++++++++++--- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .ci/updatecli/values.d/update-compose.yml diff --git a/.ci/updatecli/values.d/scm.yml b/.ci/updatecli/values.d/scm.yml index 876325fb5..a24b052d7 100644 --- a/.ci/updatecli/values.d/scm.yml +++ b/.ci/updatecli/values.d/scm.yml @@ -3,5 +3,8 @@ scm: owner: elastic repository: apm-agent-dotnet branch: main - -signedcommit: true \ No newline at end of file + commitusingapi: true + # begin update-compose policy values + user: obltmachine + email: obltmachine@users.noreply.github.com + # end update-compose policy values diff --git a/.ci/updatecli/values.d/update-compose.yml b/.ci/updatecli/values.d/update-compose.yml new file mode 100644 index 000000000..8804f9661 --- /dev/null +++ b/.ci/updatecli/values.d/update-compose.yml @@ -0,0 +1,3 @@ +spec: + files: + - "update-compose.yaml" \ No newline at end of file diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 4f7219c5e..27a7e8378 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -38,12 +38,18 @@ jobs: - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose diff + # TODO: update to the latest version so the policies can work as expected. + # latest changes in the policies require to use the dependson feature. + version: "v0.88.0" env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose apply + # TODO: update to the latest version so the policies can work as expected. + # latest changes in the policies require to use the dependson feature. + version: "v0.88.0" env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} diff --git a/update-compose.yaml b/update-compose.yaml index d40020933..e27710ace 100644 --- a/update-compose.yaml +++ b/update-compose.yaml @@ -1,18 +1,26 @@ +# Config file for `updatecli compose ...`. +# https://www.updatecli.io/docs/core/compose/ policies: - name: Handle apm-data server specs - policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-data-spec:0.2.0@sha256:7069c0773d44a74c4c8103b4d9957b468f66081ee9d677238072fe11c4d2197c + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-data-spec:0.6.0@sha256:c0bbdec23541bed38df1342c95aeb601530a113db1ff11715c1c7616ed5e9e8b values: - .ci/updatecli/values.d/scm.yml - .ci/updatecli/values.d/apm-data-spec.yml - name: Handle apm gherkin specs - policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-gherkin:0.2.0@sha256:26a30ad2b98a6e4cb17fb88a28fa3277ced8ca862d6388943afaafbf8ee96e7d + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-gherkin:0.6.0@sha256:dbaf4d855c5c212c3b5a8d2cc98c243a2b769ac347198ae8814393a1a0576587 values: - .ci/updatecli/values.d/scm.yml - .ci/updatecli/values.d/apm-gherkin.yml - name: Handle apm json specs - policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-json-specs:0.2.0@sha256:969a6d21eabd6ebea66cb29b35294a273d6dbc0f7da78589c416aedf08728e78 + policy: ghcr.io/elastic/oblt-updatecli-policies/apm/apm-json-specs:0.6.0@sha256:e5a74c159ceed02fd20515ea76fa25ff81e3ccf977e74e636f9973db86aa52a5 values: - .ci/updatecli/values.d/scm.yml - .ci/updatecli/values.d/apm-json-specs.yml + + - name: Update Updatecli policies + policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.6.0@sha256:6bd6999620674b2fbb1d374f7a1a5e9740d042667f0592900b44259f3e1ae98f + values: + - .ci/updatecli/values.d/scm.yml + - .ci/updatecli/values.d/update-compose.yml \ No newline at end of file From 32f9e6b40073ac3b70aa3ba673acddcc2b9c4e2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:07:34 +0000 Subject: [PATCH 62/77] Bump docker/build-push-action from 6.9.0 to 6.10.0 in the github-actions group (#2509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 6.9.0 to 6.10.0

    Release notes

    Sourced from docker/build-push-action's releases.

    v6.10.0

    Full Changelog: https://github.com/docker/build-push-action/compare/v6.9.0...v6.10.0

    Commits
    • 48aba3b Merge pull request #1268 from docker/dependabot/npm_and_yarn/docker/actions-t...
    • 678328c chore: update generated content
    • cdf0a37 chore(deps): Bump @​docker/actions-toolkit from 0.39.0 to 0.46.0
    • d719b79 Merge pull request #1238 from docker/dependabot/npm_and_yarn/actions/core-1.11.1
    • c333dfd chore: update generated content
    • 6b56a4c chore(deps): Bump @​actions/core from 1.10.1 to 1.11.1
    • 92fb0d7 Merge pull request #1259 from docker/dependabot/github_actions/codecov/codeco...
    • 40532c5 ci: fix deprecated input for codecov-action
    • 70dd953 Merge pull request #1267 from crazy-max/fix-allow
    • 41b4e80 Merge pull request #1261 from docker/dependabot/npm_and_yarn/cross-spawn-7.0.6
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/build-push-action&package-manager=github_actions&previous-version=6.9.0&new-version=6.10.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index d9f3f64b8..8220e2a92 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -72,7 +72,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6773bcb4b..376777f56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 with: cache-from: type=gha cache-to: type=gha,mode=max From 706aaf153380ed5d0a241f0339a117c765241724 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 2 Dec 2024 13:11:33 +0000 Subject: [PATCH 63/77] Update changelog for 1.31.0 release (#2514) As titled. --- CHANGELOG.asciidoc | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 67d7eaed5..3417e7382 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -23,6 +23,29 @@ endif::[] [[release-notes-1.x]] === .NET Agent version 1.x +[[release-notes-1.31.0]] +==== 1.31.0 - 2024/12/02 + +[float] +===== Breaking changes + +{pull}2498[#2498] Remove net 6.0 targets + +We no longer ship `net6.0` targets as .NET 6 is now out of support. Applications +targetting `net6.0` will continue to work, but fall down to the `netstandard2.0` +target which may not be as optimised. We therefore recommend updating your application +to `net8.0` or `net9.0` prior to installing 1.31.0 of the Elastic.Apm.* packages. + +===== Bug fixes + +{pull}2505[#2505] Fixes and enhancements for Azure Functions +{pull}2508[#2508] Azure Function service name logic + +[float] +===== Features + +{pull}2503[#2503] Phase one logger optimisations + [[release-notes-1.30.1]] ==== 1.30.1 - 2024/11/19 From aa8e3ba9f7967edc621df38010dad1e7fbdad181 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 2 Dec 2024 13:58:40 +0000 Subject: [PATCH 64/77] Make deploy.sh executable (#2515) --- .ci/deploy.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .ci/deploy.sh diff --git a/.ci/deploy.sh b/.ci/deploy.sh old mode 100644 new mode 100755 From def884a806322f20d3c44dac29dd14dc133496d8 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 2 Dec 2024 17:45:48 +0000 Subject: [PATCH 65/77] [DOCS] Troubleshooting for ASE (#2473) (#2517) PR #2473 was merged into 1.x rather than main in error. This syncs the changes back to main. Co-authored-by: Luca Belluccini --- docs/troubleshooting.asciidoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/troubleshooting.asciidoc b/docs/troubleshooting.asciidoc index 2e9d962f0..ce4dea0d7 100644 --- a/docs/troubleshooting.asciidoc +++ b/docs/troubleshooting.asciidoc @@ -283,4 +283,15 @@ Should be changed to: image::./images/integrated-pipeline.png[Integrated Managed Pipeline Mode in Properties] -You may need to restart Visual Studio for these changes to fully apply. \ No newline at end of file +You may need to restart Visual Studio for these changes to fully apply. + +[float] +[[azure-app-services-traceparent]] +=== Azure App Services / ASE and sampling + +Azure/ASE adds trace headers set to unsampled for incoming requests to the cluster. + +Elastic APM .NET Agent (and likely also other agents which correctly handle the W3C Trace Parent header sampling) will obey to the traceparent header sampling of the incoming request. +As a consequence, the traces/spans will not be sent to APM Server/Integration and therefore APM UI will not show anything. + +We recommend using `trace_continuation_strategy` set to `restart` or `restart_external`, so that the APM .NET Agent will ignore the incoming traceparent header sampling flag. From 831f85f1626262acb31bc7d210942bd031bd4e3c Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 2 Dec 2024 18:30:32 +0000 Subject: [PATCH 66/77] Update test dependencies (#2518) Updates to the latest test runners and dependencies. This may improve the stability of the integration tests and is a move toward updating to the .NET 9 SDK. --- Directory.Packages.props | 36 +++++++++---------- build/scripts/Tooling.fs | 23 ++++++------ dotnet-tools.json | 2 +- src/Elastic.Apm/Elastic.Apm.csproj | 1 + .../ShouldWaitDurationExtensions.cs | 10 +++--- .../XUnit/DockerAttributes.cs | 2 +- .../CentralConfigResponseParserTests.cs | 2 +- .../BackendCommTests/PayloadSenderTests.cs | 2 +- test/Elastic.Apm.Tests/Config/ConfigTests.cs | 3 +- .../Extensions/EnumerableExtensionsTests.cs | 2 +- .../HelpersTests/AgentSpinLockTests.cs | 4 +-- .../HelpersTests/ExceptionUtilsTests.cs | 2 +- .../HelpersTests/LazyContextualInitTests.cs | 10 ++++-- .../LazyContextualInitTestsHelpers.cs | 15 ++++++++ .../HelpersTests/TimeUtilsTests.cs | 2 +- test/Elastic.Apm.Tests/SamplerTests.cs | 2 +- test/Elastic.Apm.Tests/SerializationTests.cs | 2 +- .../TestsBase.cs | 2 +- .../BodyCapturingTests.cs | 2 +- .../AspNetCore/AspNetCoreTests.cs | 2 +- .../DotnetProject.cs | 10 +++--- 21 files changed, 78 insertions(+), 58 deletions(-) create mode 100644 test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTestsHelpers.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 86864e2b8..850902ccc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,7 @@ true - + @@ -29,18 +29,18 @@ - - + + - + - + @@ -54,7 +54,7 @@ - + @@ -63,18 +63,18 @@ - - - - - - - - - + + + + + + + + + - - + + @@ -117,7 +117,7 @@ - + diff --git a/build/scripts/Tooling.fs b/build/scripts/Tooling.fs index 445111974..d91bee954 100644 --- a/build/scripts/Tooling.fs +++ b/build/scripts/Tooling.fs @@ -20,33 +20,30 @@ module Tooling = let private defaultConsoleWriter = Some(ConsoleOutColorWriter() :> IConsoleOutWriter) - let private readInWithTimeout timeout workingDir bin (writer: IConsoleOutWriter option) args = + let private readInWithTimeout (timeout :TimeSpan) workingDir bin (writer: IConsoleOutWriter option) args = let startArgs = StartArguments(bin, args |> List.toArray) + startArgs.Timeout <- timeout + startArgs.ConsoleOutWriter <- Option.defaultValue (NoopWriter()) writer if (Option.isSome workingDir) then startArgs.WorkingDirectory <- Option.defaultValue "" workingDir - let result = Proc.Start(startArgs, timeout, Option.defaultValue (NoopWriter()) writer) + let result = Proc.Start(startArgs) if not result.Completed then failwithf "process failed to complete within %O: %s" timeout bin if not result.ExitCode.HasValue then failwithf "process yielded no exit code: %s" bin { ExitCode = result.ExitCode.Value; Output = seq result.ConsoleOut} - - let private read bin args = readInWithTimeout defaultTimeout None bin defaultConsoleWriter args - let private readQuiet bin args = readInWithTimeout defaultTimeout None bin None args - - let private execInWithTimeout timeout workingDir bin args = + + let private execInWithTimeout (timeout :TimeSpan) workingDir bin args = let startArgs = ExecArguments(bin, args |> List.toArray) + startArgs.Timeout <- timeout if (Option.isSome workingDir) then startArgs.WorkingDirectory <- Option.defaultValue "" workingDir - let result = Proc.Exec(startArgs, timeout) + let result = Proc.Exec(startArgs) try - if not result.HasValue || result.Value > 0 then - failwithf "process returned %i: %s" result.Value bin + if result > 0 then + failwithf "process returned %i: %s" result bin with | :? ProcExecException as ex -> failwithf "%s" ex.Message - let private execIn workingDir bin args = execInWithTimeout defaultTimeout workingDir bin args - let private exec bin args = execIn None bin args - type BuildTooling(timeout, path) = let timeout = match timeout with | Some t -> t | None -> defaultTimeout member this.Path = path diff --git a/dotnet-tools.json b/dotnet-tools.json index e5fd17baf..fd336aeb5 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "minver-cli": { - "version": "4.3.0", + "version": "6.0.0", "commands": [ "minver" ] diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index 361acc507..c23a8041e 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -46,6 +46,7 @@ + diff --git a/test/Elastic.Apm.Tests.Utilities/ShouldWaitDurationExtensions.cs b/test/Elastic.Apm.Tests.Utilities/ShouldWaitDurationExtensions.cs index d35953114..e91c8c2af 100644 --- a/test/Elastic.Apm.Tests.Utilities/ShouldWaitDurationExtensions.cs +++ b/test/Elastic.Apm.Tests.Utilities/ShouldWaitDurationExtensions.cs @@ -9,13 +9,13 @@ namespace Elastic.Apm.Tests.Utilities { public static class ShouldWaitDurationExtensions { - public static AndConstraint> + public static AndConstraint> BeGreaterOrEqualToMinimumSleepLength(this NullableNumericAssertions duration) => - duration.NotBeNull().And.BeGreaterOrEqualTo(WaitHelpers.SleepLength); + duration.NotBeNull().And.BeGreaterOrEqualTo(WaitHelpers.SleepLength); - public static AndConstraint> BeGreaterOrEqualToMinimumSleepLength(this NullableNumericAssertions duration, - int numberOfSleeps - ) + public static AndConstraint> BeGreaterOrEqualToMinimumSleepLength( + this NullableNumericAssertions duration, + int numberOfSleeps) { var expectedTransactionLength = numberOfSleeps * WaitHelpers.SleepLength; return duration.NotBeNull() diff --git a/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs b/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs index 8332ea684..ccee058a2 100644 --- a/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs +++ b/test/Elastic.Apm.Tests.Utilities/XUnit/DockerAttributes.cs @@ -67,7 +67,7 @@ static DockerUtils() { try { - var result = Proc.Start(new StartArguments("docker", "--version"), TimeSpan.FromSeconds(30)); + var result = Proc.Start(new StartArguments("docker", "--version") { Timeout = TimeSpan.FromSeconds(30) }); HasDockerInstalled = result.ExitCode == 0; } catch (Exception) diff --git a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigResponseParserTests.cs b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigResponseParserTests.cs index a0b8ed89a..ea66a245d 100644 --- a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigResponseParserTests.cs +++ b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigResponseParserTests.cs @@ -305,7 +305,7 @@ public static IEnumerable ConfigDeltaData { cfg.LogLevel.Should() .NotBeNull() - .And.Be(value); + .And.Be((LogLevel)value); }) }; } diff --git a/test/Elastic.Apm.Tests/BackendCommTests/PayloadSenderTests.cs b/test/Elastic.Apm.Tests/BackendCommTests/PayloadSenderTests.cs index a83e0d1a0..4bd44a4a2 100644 --- a/test/Elastic.Apm.Tests/BackendCommTests/PayloadSenderTests.cs +++ b/test/Elastic.Apm.Tests/BackendCommTests/PayloadSenderTests.cs @@ -320,7 +320,7 @@ public static IEnumerable FlushInterval_test_variants() [Theory] [MemberData(nameof(FlushInterval_test_variants))] - internal async void FlushInterval_test(TestArgs args, int numberOfEventsToSend) + internal async Task FlushInterval_test(TestArgs args, int numberOfEventsToSend) { var batchSentBarrier = new Barrier(2); var barrierTimeout = 30.Seconds(); diff --git a/test/Elastic.Apm.Tests/Config/ConfigTests.cs b/test/Elastic.Apm.Tests/Config/ConfigTests.cs index 7a7b829d3..e63e39d74 100644 --- a/test/Elastic.Apm.Tests/Config/ConfigTests.cs +++ b/test/Elastic.Apm.Tests/Config/ConfigTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Elastic.Apm.Config; using Elastic.Apm.Helpers; using Elastic.Apm.Logging; @@ -30,7 +31,7 @@ namespace Elastic.Apm.Tests.Config [Collection("UsesEnvironmentVariables")] public class ConfigTests : IDisposable { - public static TheoryData GlobalLabelsValidVariantsToTest => new TheoryData> + public static TheoryData> GlobalLabelsValidVariantsToTest => new TheoryData> { // empty string - zero key value pairs { "", new Dictionary() }, diff --git a/test/Elastic.Apm.Tests/Extensions/EnumerableExtensionsTests.cs b/test/Elastic.Apm.Tests/Extensions/EnumerableExtensionsTests.cs index 7488f40d3..c992ba20e 100644 --- a/test/Elastic.Apm.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/test/Elastic.Apm.Tests/Extensions/EnumerableExtensionsTests.cs @@ -14,7 +14,7 @@ namespace Elastic.Apm.Tests.Extensions { public class EnumerableExtensionsTests { - public static TheoryData EnumerablesToTest => new TheoryData, object[]> + public static TheoryData, object[]> EnumerablesToTest => new TheoryData, object[]> { { Array.Empty(), Array.Empty() }, { Enumerable.Range(0, 0).Select(i => (object)i), Array.Empty() }, diff --git a/test/Elastic.Apm.Tests/HelpersTests/AgentSpinLockTests.cs b/test/Elastic.Apm.Tests/HelpersTests/AgentSpinLockTests.cs index 99f9af7fa..f4cf966cb 100644 --- a/test/Elastic.Apm.Tests/HelpersTests/AgentSpinLockTests.cs +++ b/test/Elastic.Apm.Tests/HelpersTests/AgentSpinLockTests.cs @@ -29,7 +29,7 @@ public class AgentSpinLockTests : LoggingTestBase public AgentSpinLockTests(ITestOutputHelper xUnitOutputHelper) : base(xUnitOutputHelper) => _logger = LoggerBase.Scoped(ThisClassName); - internal interface ISpinLockForTest + public interface ISpinLockForTest { void Release(); @@ -45,7 +45,7 @@ internal interface ISpinLockForTest new NoopSpinLockForTest() }; - public static TheoryData ThreadSafeSpinLockImpls => new TheoryData { new AgentSpinLockForTest() }; + public static TheoryData ThreadSafeSpinLockImpls => [new AgentSpinLockForTest()]; [Fact] public void default_value_is_false() diff --git a/test/Elastic.Apm.Tests/HelpersTests/ExceptionUtilsTests.cs b/test/Elastic.Apm.Tests/HelpersTests/ExceptionUtilsTests.cs index d36f5912c..61af79273 100644 --- a/test/Elastic.Apm.Tests/HelpersTests/ExceptionUtilsTests.cs +++ b/test/Elastic.Apm.Tests/HelpersTests/ExceptionUtilsTests.cs @@ -14,7 +14,7 @@ namespace Elastic.Apm.Tests.HelpersTests { public class ExceptionUtilsTests { - public static TheoryData DoSwallowingExceptionsVariantsToTest => new TheoryData + public static TheoryData DoSwallowingExceptionsVariantsToTest => new TheoryData { { ExceptionUtils.MethodExitingNormallyMsgFmt, () => { } }, { ExceptionUtils.MethodExitingCancelledMsgFmt, () => new CancellationToken(true).ThrowIfCancellationRequested() }, diff --git a/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTests.cs b/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTests.cs index ec2f1f0b1..51e37f326 100644 --- a/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTests.cs +++ b/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTests.cs @@ -14,7 +14,7 @@ namespace Elastic.Apm.Tests.HelpersTests { public class LazyContextualInitTests { - public static TheoryData WaysToCallInit = new TheoryData> + public static readonly TheoryData WaysToCallInit = new TheoryData>() { { "IfNotInited?.Init ?? false", (lazyCtxInit, initAction) => lazyCtxInit.IfNotInited?.Init(initAction) ?? false }, { "Init", (lazyCtxInit, initAction) => lazyCtxInit.Init(initAction) } @@ -22,8 +22,8 @@ public class LazyContextualInitTests [Theory] [MemberData(nameof(WaysToCallInitOrGetString))] - internal void with_result_initialized_only_once_on_first_call(string dbgWayToCallDesc - , Func, Func, string> wayToCall + internal void with_result_initialized_only_once_on_first_call(string dbgWayToCallDesc, + Func, Func, string> wayToCall ) { var counter = new ThreadSafeIntCounter(); @@ -58,7 +58,9 @@ internal void with_result_multiple_threads(string dbgWayToCallDesc, Func wayToCall) { var counter = new ThreadSafeIntCounter(); @@ -81,7 +83,9 @@ internal void no_result_initialized_only_once_on_first_call(string dbgWayToCallD } [Theory] +#pragma warning disable xUnit1037 // There are fewer theory data type arguments than required by the parameters of the test method [MemberData(nameof(WaysToCallInit))] +#pragma warning restore xUnit1037 // There are fewer theory data type arguments than required by the parameters of the test method internal void no_result_multiple_threads(string dbgWayToCallDesc, Func wayToCall) { var counter = new ThreadSafeIntCounter(); diff --git a/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTestsHelpers.cs b/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTestsHelpers.cs new file mode 100644 index 000000000..c587fc7d3 --- /dev/null +++ b/test/Elastic.Apm.Tests/HelpersTests/LazyContextualInitTestsHelpers.cs @@ -0,0 +1,15 @@ +using System; +using Elastic.Apm.Helpers; +using Xunit; +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +internal static class LazyContextualInitTestsHelpers +{ + public static TheoryData> WaysToCallInit = new() + { + { "IfNotInited?.Init ?? false", (lazyCtxInit, initAction) => lazyCtxInit.IfNotInited?.Init(initAction) ?? false }, + { "Init", (lazyCtxInit, initAction) => lazyCtxInit.Init(initAction) } + }; +} diff --git a/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs b/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs index 54ace11cb..c131d3dae 100644 --- a/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs +++ b/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs @@ -14,7 +14,7 @@ public class TimeUtilsTests { private static readonly DateTime UnixEpochDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - public static TheoryData TimestampAndDateTimeVariantsToTest => new TheoryData + public static TheoryData TimestampAndDateTimeVariantsToTest => new TheoryData { { 0, UnixEpochDateTime }, { 1, UnixEpochDateTime + TimeUtils.TimeSpanFromFractionalMilliseconds(0.001) }, diff --git a/test/Elastic.Apm.Tests/SamplerTests.cs b/test/Elastic.Apm.Tests/SamplerTests.cs index f63d0283d..b1d39416f 100644 --- a/test/Elastic.Apm.Tests/SamplerTests.cs +++ b/test/Elastic.Apm.Tests/SamplerTests.cs @@ -16,7 +16,7 @@ namespace Elastic.Apm.Tests public class SamplerTests { // ReSharper disable once MemberCanBePrivate.Global - public static TheoryData RateVariantsToTest => new TheoryData + public static TheoryData RateVariantsToTest => new TheoryData { 0, 0.0001, diff --git a/test/Elastic.Apm.Tests/SerializationTests.cs b/test/Elastic.Apm.Tests/SerializationTests.cs index 7ae39c0e1..d24381168 100644 --- a/test/Elastic.Apm.Tests/SerializationTests.cs +++ b/test/Elastic.Apm.Tests/SerializationTests.cs @@ -31,7 +31,7 @@ public SerializationTests() => _payloadItemSerializer = new PayloadItemSerializer(); // ReSharper disable once MemberCanBePrivate.Global - public static TheoryData SerializationUtilsTrimToPropertyMaxLengthVariantsToTest => new TheoryData + public static TheoryData SerializationUtilsTrimToPropertyMaxLengthVariantsToTest => new TheoryData { { "", "" }, { "A", "A" }, diff --git a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs index 3612af656..636b01c3f 100644 --- a/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs @@ -322,7 +322,7 @@ int expectedStatusCode (int)response.StatusCode, response.StatusCode); try { - response.StatusCode.Should().Be(expectedStatusCode); + response.StatusCode.Should().Be((HttpStatusCode)expectedStatusCode); } catch (XunitException ex) { diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs index dd6f5ca78..34bee5535 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs @@ -95,7 +95,7 @@ public async Task ComplexDataSendCaptureBody() var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json")); // make sure the sample app received the data - result.StatusCode.Should().Be(200); + result.StatusCode.Should().Be((HttpStatusCode)200); // and make sure the data is captured by the agent sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull(); diff --git a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AspNetCore/AspNetCoreTests.cs b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AspNetCore/AspNetCoreTests.cs index eb744f155..38e1889c2 100644 --- a/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AspNetCore/AspNetCoreTests.cs +++ b/test/profiler/Elastic.Apm.Profiler.Managed.Tests/AspNetCore/AspNetCoreTests.cs @@ -26,7 +26,7 @@ public class AspNetCoreTests /// [Theory] [InlineData("net8.0")] - public async void AspNetCoreTest(string framework) + public async System.Threading.Tasks.Task AspNetCoreTest(string framework) { var apmLogger = new InMemoryBlockingLogger(Logging.LogLevel.Error); var apmServer = new MockApmServer(apmLogger, nameof(AspNetCoreTests)); diff --git a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs index 43cb28b3a..0f6758bd8 100644 --- a/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs +++ b/test/startuphook/Elastic.Apm.StartupHook.Tests/DotnetProject.cs @@ -73,10 +73,11 @@ private bool TryPublish() var startArgs = new StartArguments("dotnet", args) { - WorkingDirectory = workingDirectory + WorkingDirectory = workingDirectory, + Timeout = TimeSpan.FromSeconds(30) }; - var publishResult = Proc.Start(startArgs, TimeSpan.FromSeconds(30)); + var publishResult = Proc.Start(startArgs); foreach (var line in publishResult.ConsoleOut) { @@ -193,8 +194,9 @@ public static DotnetProject Create(ITestOutputHelper output, string name, string var result = Proc.Start(new StartArguments("dotnet", args) { - WorkingDirectory = directory - }, TimeSpan.FromSeconds(30)); + WorkingDirectory = directory, + Timeout = TimeSpan.FromSeconds(30) + }); if (result.Completed) { From a587c86873e998f6cc1a04bbcb77d728e7aaca29 Mon Sep 17 00:00:00 2001 From: "elastic-observability-automation[bot]" <180520183+elastic-observability-automation[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:38:30 +0100 Subject: [PATCH 67/77] deps(updatecli): bump all policies (#2519) --- update-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/update-compose.yaml b/update-compose.yaml index e27710ace..aecdf5d75 100644 --- a/update-compose.yaml +++ b/update-compose.yaml @@ -20,7 +20,7 @@ policies: - .ci/updatecli/values.d/apm-json-specs.yml - name: Update Updatecli policies - policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.6.0@sha256:6bd6999620674b2fbb1d374f7a1a5e9740d042667f0592900b44259f3e1ae98f + policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.8.0@sha256:99e9e61b501575c2c176c39f2275998d198b590a3f6b1fe829f7315f8d457e7f values: - .ci/updatecli/values.d/scm.yml - - .ci/updatecli/values.d/update-compose.yml \ No newline at end of file + - .ci/updatecli/values.d/update-compose.yml From cfd48fc61e388acab994695d1a02d07b762da714 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 3 Dec 2024 14:32:01 +0100 Subject: [PATCH 68/77] ci: pin updatecli version using .tool-versions and autobump (#2522) --- .github/workflows/updatecli.yml | 8 ++------ .tool-versions | 1 + update-compose.yaml | 5 +++++ 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 .tool-versions diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 27a7e8378..9b169a707 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -38,18 +38,14 @@ jobs: - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose diff - # TODO: update to the latest version so the policies can work as expected. - # latest changes in the policies require to use the dependson feature. - version: "v0.88.0" + version-file: .tool-versions env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose apply - # TODO: update to the latest version so the policies can work as expected. - # latest changes in the policies require to use the dependson feature. - version: "v0.88.0" + version-file: .tool-versions env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..3d067142f --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +updatecli v0.88.0 \ No newline at end of file diff --git a/update-compose.yaml b/update-compose.yaml index aecdf5d75..aeae96a1b 100644 --- a/update-compose.yaml +++ b/update-compose.yaml @@ -24,3 +24,8 @@ policies: values: - .ci/updatecli/values.d/scm.yml - .ci/updatecli/values.d/update-compose.yml + + - name: Update Updatecli version + policy: ghcr.io/elastic/oblt-updatecli-policies/updatecli/version:0.2.0@sha256:013a37ddcdb627c46e7cba6fb9d1d7bc144584fa9063843ae7ee0f6ef26b4bea + values: + - .ci/updatecli/values.d/scm.yml From ed42e120e55a01420bd39fb6b3251e8b7238f00c Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 3 Dec 2024 16:13:33 +0100 Subject: [PATCH 69/77] triage: exclude elastic-observability-automation[bot] (#2520) --- .github/workflows/labeler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 97001eb79..353f84697 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -34,7 +34,7 @@ jobs: github-token: ${{ steps.get_token.outputs.token }} - name: Add community and triage labels - if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' + if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' && github.actor != 'elastic-observability-automation[bot]' uses: actions/github-script@v7 with: script: | @@ -46,7 +46,7 @@ jobs: }) - name: Add comment for community PR - if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' + if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' && github.actor != 'elastic-observability-automation[bot]' uses: wow-actions/auto-comment@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 12fe9d60f9c18f2c00e66a25bd8255ad7cddaa1c Mon Sep 17 00:00:00 2001 From: "elastic-observability-automation[bot]" <180520183+elastic-observability-automation[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:28:16 +0000 Subject: [PATCH 70/77] deps: Bump updatecli version to v0.88.1 (#2523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit

    deps: Bump updatecli version

    deps(updatecli): Bump updatecli version to v0.88.1

    1 file(s) updated with "updatecli v0.88.1": * .tool-versions

    v0.88.1
    Release published on the 2024-12-02 12:05:12 +0000 UTC at the
    url
    https://github.com/updatecli/updatecli/releases/tag/v0.88.1
    
    ##
    Changes
    
    ## 🐛 Bug Fixes
    
    -
    feat(udash): allow to login on Udash without auth enabled @olblak
    (#3203)
    - fix(udash): display target console output in
    Updatecli report @olblak (#3190)
    
    ## 🧰
    Maintenance
    
    - deps(go): bump module
    github.com/zclconf/go-cty @updateclibot (#3182)
    
    ##
    Contributors
    
    @olblak, @updateclibot and
    @updateclibot[bot]
    
    GitHub Action workflow link
    ---
    Updatecli
logo

    Created automatically by Updatecli

    Options:

    Most of Updatecli configuration is done via its manifest(s).

    • If you close this pull request, Updatecli will automatically reopen it, the next time it runs.
    • If you close this pull request and delete the base branch, Updatecli will automatically recreate it, erasing all previous commits made.

    Feel free to report any issues at github.com/updatecli/updatecli.
    If you find this tool useful, do not hesitate to star our GitHub repository as a sign of appreciation, and/or to tell us directly on our chat!

    Co-authored-by: elastic-observability-automation[bot] <180520183+elastic-observability-automation[bot]@users.noreply.github.com> --- .tool-versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index 3d067142f..058189984 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -updatecli v0.88.0 \ No newline at end of file +updatecli v0.88.1 \ No newline at end of file From 6f0987ebc97e94ac42345510d7d9bcffcee53006 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 9 Dec 2024 13:44:19 +0000 Subject: [PATCH 71/77] Skip dotnet format on Windows (#2527) As titled --- .github/workflows/test-windows.yml | 41 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 145e86bbf..99cd25720 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -45,25 +45,28 @@ jobs: - name: Format run: ./build.bat format + # Skipping due to this failing consistently - See https://github.com/elastic/apm-agent-dotnet/issues/2524 + # TODO - Re-enable when possible. #required step - tests: - runs-on: windows-2022 - needs: [ 'format' ] - timeout-minutes: 30 + # tests: + # runs-on: windows-2022 + # needs: [ 'format' ] + # timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: Bootstrap Action Workspace - uses: ./.github/workflows/bootstrap - with: - tc-cloud: ${{ secrets.TC_CLOUD_TOKEN }} - - - name: 'Tests: Unit' - run: ./build.bat test --test-suite unit + # steps: + # - uses: actions/checkout@v4 + # - name: Bootstrap Action Workspace + # uses: ./.github/workflows/bootstrap + # with: + # tc-cloud: ${{ secrets.TC_CLOUD_TOKEN }} + + # - name: 'Tests: Unit' + # run: ./build.bat test --test-suite unit integrations-tests: runs-on: windows-2022 - needs: [ 'format', 'tests' ] + #needs: [ 'format', 'tests' ] + needs: [ 'tests' ] steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -84,7 +87,8 @@ jobs: startup-hook-tests: runs-on: windows-2022 - needs: [ 'format', 'tests' ] + #needs: [ 'format', 'tests' ] + needs: [ 'tests' ] steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -95,7 +99,8 @@ jobs: profiler-tests: runs-on: windows-2022 - needs: [ 'format', 'tests' ] + #needs: [ 'format', 'tests' ] + needs: [ 'tests' ] steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -109,8 +114,8 @@ jobs: test-iis: runs-on: windows-latest - needs: [ 'format', 'tests' ] - + #needs: [ 'format', 'tests' ] + needs: [ 'tests' ] steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace From ea38690430d302317f9d610435fa0467bb1b8217 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:16:50 +0000 Subject: [PATCH 72/77] Bump actions/attest-build-provenance from 1.4.4 to 2.0.1 in the github-actions group (#2525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/attest-build-provenance` from 1.4.4 to 2.0.1
    Release notes

    Sourced from actions/attest-build-provenance's releases.

    v2.0.1

    What's Changed

    Full Changelog: https://github.com/actions/attest-build-provenance/compare/v2.0.0...v2.0.1

    v2.0.0

    The attest-build-provenance action now supports attesting multiple subjects simultaneously. When identifying multiple subjects with the subject-path input a single attestation is created with references to each of the supplied subjects, rather than generating separate attestations for each artifact. This reduces the number of attestations that you need to create and manage.

    What's Changed

    Full Changelog: https://github.com/actions/attest-build-provenance/compare/v1.4.4...v2.0.0

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/attest-build-provenance&package-manager=github_actions&previous-version=1.4.4&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 8220e2a92..e44db1a35 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -35,7 +35,7 @@ jobs: run: ./build.sh pack - name: generate build provenance - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/build/output/_packages/*.nupkg" @@ -85,7 +85,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -93,12 +93,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 376777f56..04b0b4e3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,7 +78,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -86,12 +86,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" @@ -147,7 +147,7 @@ jobs: run: ./build.bat profiler-zip - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_ZIP_FILE }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_ZIP_FILE }}" From 2474a5d6b232c4b904fa519485d99b498023d3cb Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 10 Dec 2024 09:12:38 +0000 Subject: [PATCH 73/77] Update ASP.NET Core documentation (#2526) Update ASP.NET Core documentation to prefer newer APIs --- docs/configuration.asciidoc | 55 ++++++++++++++------------------ docs/packages.asciidoc | 21 +++++------- docs/public-api.asciidoc | 9 ++++-- docs/setup-asp-net-core.asciidoc | 43 ++++++++++++------------- docs/troubleshooting.asciidoc | 23 +++++++++---- 5 files changed, 74 insertions(+), 77 deletions(-) diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 84fe6f1f0..4d92ac5cd 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -6,9 +6,11 @@ endif::[] [[configuration]] == Configuration -Utilize configuration options to adapt the Elastic APM agent to your needs. There are multiple configuration sources, each with different naming conventions for the property key. +Utilize configuration options to adapt the Elastic APM agent to your needs. There are multiple configuration sources, +each with different naming conventions for the property key. -By default, the agent uses environment variables. Additionally, on ASP.NET Core, the agent can plug into the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.2[Microsoft.Extensions.Configuration] infrastructure. +By default, the agent uses environment variables. Additionally, on ASP.NET Core, the agent plugs +into the https://learn.microsoft.com/aspnet/core/fundamentals/configuration[Microsoft.Extensions.Configuration] infrastructure. [float] [[dynamic-configuration]] @@ -24,47 +26,39 @@ This feature is enabled in the Agent by default, with <>. [[configuration-on-asp-net-core]] === Configuration on ASP.NET Core -The `UseElasticApm()` extension method offers an overload to pass an `IConfiguration` instance to the APM Agent. -To use this type of setup, which is typical in an ASP.NET Core application, your application's `Startup.cs` file should contain code similar to the following: +The `AddElasticApm()` extension method on the `IServiceCollection` automatically accesses configuration bound via +the `Microsoft.Extensions.Configuration` sources. To use this type of setup, which is typical in an ASP.NET Core application, +your application's `Program.cs` file should contain code similar to the following: [source,csharp] ---- -using Elastic.Apm.AspNetCore; +var builder = WebApplication.CreateBuilder(args); -public class Startup -{ - private readonly IConfiguration _configuration; +builder.Services.AddAllElasticApm(); - public Startup(IConfiguration configuration) - { - _configuration = configuration; - } +var app = builder.Build(); - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - //Registers the agent with an IConfiguration instance: - app.UseElasticApm(_configuration); +// Configure the HTTP request pipeline. - //Rest of the Configure() method... - } -} +app.Run(); ---- With this setup, the Agent is able to be configured in the same way as any other library in your application. -For example, any configuration source that has been configured on the `IConfiguration` instance being passed to the APM Agent can be used to set Agent configuration values. +For example, any configuration source that has been configured on the `IConfiguration` instance in use in the application + can be used to set Agent configuration values. -More information is available in the official https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1[Microsoft .NET Core configuration docs] +More information is available in the official https://learn.microsoft.com/aspnet/core/fundamentals/configuration[Microsoft .NET Core configuration docs] You can find the key for each APM configuration option in this documentation, under the `IConfiguration or Web.config key` column of the option's description. -NOTE: It is also possible to call `UseElasticApm()` without the overload. In this case, the agent will only read configurations from environment variables. - -NOTE: The `UseElasticApm` method only turns on ASP.NET Core monitoring. To turn on tracing for everything supported by the Agent on .NET Core, including HTTP and database monitoring, use the `UseAllElasticApm` method from the `Elastic.Apm NetCoreAll` package. Learn more in <>. +NOTE: The `AddElasticApm` method only turns on ASP.NET Core monitoring. To turn on tracing for everything supported by the Agent on .NET Core, including HTTP +and database monitoring, use the `AddAllElasticApm` method from the `Elastic.Apm NetCoreAll` package. Learn more in <>. [float] [[sample-config]] ==== Sample configuration file -Here is a sample `appsettings.json` configuration file for a typical ASP.NET Core application that has been activated with `UseElasticApm()`. There are two important takeaways, which are listed as callouts below the example: +Here is a sample `appsettings.json` configuration file for a typical ASP.NET Core application that has been activated with +`AddElasticApm()`. There is one important takeaway, listed as a callout below the example: [source,js] ---- @@ -76,7 +70,7 @@ Here is a sample `appsettings.json` configuration file for a typical ASP.NET Cor } }, "AllowedHosts": "*", - "ElasticApm": <2> + "ElasticApm": { "ServerUrl": "http://myapmserver:8200", "SecretToken": "apm-server-secret-token", @@ -85,9 +79,8 @@ Here is a sample `appsettings.json` configuration file for a typical ASP.NET Cor } ---- <1> With ASP.NET Core, you must set `LogLevel` for the internal APM logger in the standard `Logging` section with the `Elastic.Apm` category name. -<2> The configurations below `ElasticApm` are fetched by the agent if the corresponding `IConfiguration` is passed to the agent. -In certain scenarios--like when you're not using ASP.NET Core--you won't activate the agent with the `UseElasticApm()` method. +In certain scenarios--like when you're not using ASP.NET Core--you won't activate the agent with the `AddElasticApm()` method. In this case, set the agent log level with <>, as shown in the following `appsettings.json` file: [source,js] @@ -1356,9 +1349,9 @@ Sets the logging level for the agent. Valid options: `Critical`, `Error`, `Warning`, `Info`, `Debug`, `Trace` and `None` (`None` disables the logging). -IMPORTANT: The `UseElasticApm()` extension offers an overload to pass an `IConfiguration` instance to the agent. -When configuring your agent in this way, as is typical in an ASP.NET Core application, -you must instead set the `LogLevel` for the internal APM logger under the `Logging` section of `appsettings.json`. More details, including a <> are available in <>. +IMPORTANT: The `AddElasticApm()` extension enables configuration, as is typical in an ASP.NET Core application. +You must instead set the `LogLevel` for the internal APM logger under the `Logging` section of `appsettings.json`. +More details, including a <> are available in <>. [options="header"] |============ diff --git a/docs/packages.asciidoc b/docs/packages.asciidoc index c056a83cd..bcc820594 100644 --- a/docs/packages.asciidoc +++ b/docs/packages.asciidoc @@ -100,13 +100,12 @@ A package containing instrumentation to capture spans for commands sent to redis ==== Quick start Instrumentation can be enabled for Entity Framework Core by referencing {nuget}/Elastic.Apm.EntityFrameworkCore[`Elastic.Apm.EntityFrameworkCore`] package -and passing `EfCoreDiagnosticsSubscriber` to the `UseElasticApm` method in case of ASP.NET Core as following +and passing `EfCoreDiagnosticsSubscriber` to the `AddElasticApm` method in case of ASP.NET Core as following [source,csharp] ---- -app.UseElasticApm(Configuration, new EfCoreDiagnosticsSubscriber()); <1> +app.Services.AddElasticApm(new EfCoreDiagnosticsSubscriber()); ---- -<1> Configuration is the `IConfiguration` instance passed to your `Startup` type or passing `EfCoreDiagnosticsSubscriber` to the `Subscribe` method @@ -159,14 +158,13 @@ as this will register multiple instances, causing multiple database spans to be ==== Quick start Instrumentation can be enabled for Elasticsearch when using the official Elasticsearch clients, Elasticsearch.Net and Nest, by referencing -{nuget}/Elastic.Apm.Elasticsearch[`Elastic.Apm.Elasticsearch`] package and passing `ElasticsearchDiagnosticsSubscriber` to the `UseElasticApm` +{nuget}/Elastic.Apm.Elasticsearch[`Elastic.Apm.Elasticsearch`] package and passing `ElasticsearchDiagnosticsSubscriber` to the `AddElasticApm` method in case of ASP.NET Core as following [source,csharp] ---- -app.UseElasticApm(Configuration, new ElasticsearchDiagnosticsSubscriber()); <1> +app.Services.AddElasticApm(new ElasticsearchDiagnosticsSubscriber()); ---- -<1> Configuration is the `IConfiguration` instance passed to your `Startup` type or passing `ElasticsearchDiagnosticsSubscriber` to the `Subscribe` method @@ -194,13 +192,12 @@ Automatic instrumentation for gRPC can be enabled for both client-side and serve Automatic instrumentation for ASP.NET Core server-side is built in to <> Automatic instrumentation can be enabled for the client-side by referencing {nuget}/Elastic.Apm.GrpcClient[`Elastic.Apm.GrpcClient`] package -and passing `GrpcClientDiagnosticListener` to the `UseElasticApm` method in case of ASP.NET Core +and passing `GrpcClientDiagnosticListener` to the `AddElasticApm` method in case of ASP.NET Core [source,csharp] ---- -app.UseElasticApm(Configuration, new GrpcClientDiagnosticListener()); <1> +app.Services.AddElasticApm(new GrpcClientDiagnosticListener()); ---- -<1> Configuration is the `IConfiguration` instance passed to your `Startup` type or passing `GrpcClientDiagnosticSubscriber` to the `Subscribe` method @@ -211,7 +208,6 @@ Agent.Subscribe(new GrpcClientDiagnosticSubscriber()); Diagnostic events from `Grpc.Net.Client` are captured as spans. - [[setup-sqlclient]] === SqlClient @@ -219,14 +215,13 @@ Diagnostic events from `Grpc.Net.Client` are captured as spans. ==== Quick start You can enable auto instrumentation for `System.Data.SqlClient` or `Microsoft.Data.SqlClient` by referencing {nuget}/Elastic.Apm.SqlClient[`Elastic.Apm.SqlClient`] package -and passing `SqlClientDiagnosticSubscriber` to the `UseElasticApm` method in case of ASP.NET Core as it shown in example: +and passing `SqlClientDiagnosticSubscriber` to the `AddElasticApm` method in case of ASP.NET Core as it shown in example: [source,csharp] ---- // Enable tracing of outgoing db requests -app.UseElasticApm(Configuration, new SqlClientDiagnosticSubscriber()); <1> +app.Services.AddElasticApm(new SqlClientDiagnosticSubscriber()); ---- -<1> Configuration is the `IConfiguration` instance passed to your `Startup` type or passing `SqlClientDiagnosticSubscriber` to the `Subscribe` method and make sure that the code is called only once, otherwise the same database call could be captured multiple times: diff --git a/docs/public-api.asciidoc b/docs/public-api.asciidoc index ce1c26922..fc689de11 100644 --- a/docs/public-api.asciidoc +++ b/docs/public-api.asciidoc @@ -26,7 +26,7 @@ This implicit initialization of the agent happens on the first call on the `Elas NOTE: One exception is the `Elastic.Apm.Agent.IsConfigured` method. This method never initializes the agent, it only checks if the agent is already initialized. -Another example of initialization is when you enable the Agent with one of the technology-specific methods from the <> instructions. Specifically when the `UseElasticApm` or `UseAllElasticApm` method is called in ASP.NET Core or when the IIS module is initialized in an IIS application. +Another example of initialization is when you enable the Agent with one of the technology-specific methods from the <> instructions. Specifically when the `AddElasticApm` or `AddAllElasticApm` method is called in ASP.NET Core or when the IIS module is initialized in an IIS application. The default agent setup should cover most of the use cases and the primary way to configure the agent is through environment variables. @@ -41,7 +41,10 @@ In the AgentComponents you can pass following optional components to the agent: - `IPayloadSender`: A component that receives all the captured events like spans, transactions, and metrics. The default implementation serializes all events and sends them to the Elastic APM Server - `IConfigurationReader`: A component that reads <>. The default implementation reads configuration through environment variables. -NOTE: In the case of ASP.NET Core, when you register the agent, the `UseElasticApm` and the `UseAllElasticApm` methods both implicitly initialize the agent by calling the `Elastic.Apm.Agent.Setup` method internally. In that setup, the `IConfigurationReader` implementation will read configuration from the ASP.NET Core configuration system in case you pass an `IConfiguration` instance to the method. The `IApmLogger` instance will also log through the configured logging provider by integrating into the ASP.NET Core logging system. +NOTE: In the case of ASP.NET Core, when you register the agent, the `AddElasticApm` and the `AddAllElasticApm` methods both implicitly initialize the agent by +calling the `Elastic.Apm.Agent.Setup` method internally. In that setup, the `IConfigurationReader` implementation will read configuration from the ASP.NET Core +configuration system in case you pass an `IConfiguration` instance to the method. The `IApmLogger` instance will also log through the configured logging +provider by integrating into the ASP.NET Core logging system. [float] [[auto-instrumentation-and-agent-api]] @@ -49,7 +52,7 @@ NOTE: In the case of ASP.NET Core, when you register the agent, the `UseElasticA With the `Elastic.Apm.Agent.Subscribe(params IDiagnosticsSubscriber[] subscribers)` method you can turn on auto instrumentation for supported libraries. -In the case of ASP.NET Core, when you turn on the agent with the `UseAllElasticApm` method, the agent will do this automatically. +In the case of ASP.NET Core, when you turn on the agent with the `AddAllElasticApm` method, the agent will do this automatically. With a typical console application, you need to do this manually by calling `Elastic.Apm.Agent.Subscribe(params IDiagnosticsSubscriber[] subscribers)` method somewhere in your application, ideally in diff --git a/docs/setup-asp-net-core.asciidoc b/docs/setup-asp-net-core.asciidoc index 9008a84b1..ec94deef1 100644 --- a/docs/setup-asp-net-core.asciidoc +++ b/docs/setup-asp-net-core.asciidoc @@ -7,44 +7,41 @@ [float] ==== Quick start -[IMPORTANT] --- -We strongly suggest using the approach described in the <>, -to register the agent on the `IServiceCollection`, as opposed to using `IApplicationBuilder` as described below. +For ASP.NET Core, once you reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package, you can enable auto instrumentation +by calling the `AddAllElasticApm()` extension method on the `IServiceCollection` in the `Program.cs` file. -We keep the `IApplicationBuilder` introduced here only for backwards compatibility. +[NOTE] +-- +The following code sample assumes the instrumentation of a ASP.NET Core 8 application, using +https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/top-level-statements[top-level statements]. -- - -For ASP.NET Core, once you reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package, you can enable auto instrumentation by calling the `UseAllElasticApm()` extension method: [source,csharp] ---- -using Elastic.Apm.NetCoreAll; - -public class Startup -{ - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseAllElasticApm(Configuration); - //…rest of the method - } - //…rest of the class -} ----- +var builder = WebApplication.CreateBuilder(args); -The `app.UseAllElasticApm(...)` line **must** be the first line in the `Configure` method, otherwise the agent won't be able to properly measure the timing of your requests, and complete requests may potentially be missed by the agent. +builder.Services.AddAllElasticApm(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.Run(); +---- With this you enable every agent component including ASP.NET Core tracing, monitoring of outgoing HTTP request, Entity Framework Core database tracing, etc. -In case you only reference the {nuget}/Elastic.Apm.AspNetCore[`Elastic.Apm.AspNetCore`] package, you won't find the `UseAllElasticApm`. Instead you need to use the `UseElasticApm()` method from the `Elastic.Apm.AspNetCore` namespace. This method turns on ASP.NET Core tracing, and gives you the opportunity to manually turn on other components. By default it will only trace ASP.NET Core requests - No HTTP request tracing, database call tracing or any other tracing component will be turned on. +In case you only reference the {nuget}/Elastic.Apm.AspNetCore[`Elastic.Apm.AspNetCore`] package, you won't find the `AddAllElasticApm`. Instead you need to use +the `AddElasticApmForAspNetCore()` method. This method turns on ASP.NET Core tracing, and gives you the opportunity to manually turn on other components. By default it +will only trace ASP.NET Core requests - No HTTP request tracing, database call tracing or any other tracing component will be turned on. -In case you would like to turn on specific tracing components you can pass those to the `UseElasticApm` method. +In case you would like to turn on specific tracing components you can pass those to the `AddElasticApm` method. For example: [source,csharp] ---- -app.UseElasticApm(Configuration, +builder.Services.AddElasticApm( new HttpDiagnosticsSubscriber(), /* Enable tracing of outgoing HTTP requests */ new EfCoreDiagnosticsSubscriber()); /* Enable tracing of database calls through EF Core*/ ---- diff --git a/docs/troubleshooting.asciidoc b/docs/troubleshooting.asciidoc index ce4dea0d7..43184704e 100644 --- a/docs/troubleshooting.asciidoc +++ b/docs/troubleshooting.asciidoc @@ -89,8 +89,8 @@ The default value is `file` if `OTEL_DOTNET_AUTO_LOG_DIRECTORY` is set or `OTEL_ [[collect-logs-core]] ==== ASP.NET Core -If you added the agent to your application as per the <> document with the `UseAllElasticApm` or `UseElasticApm` method, it will integrate with the -https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging[ASP.NET Core logging infrastructure]. +If you added the agent to your application as per the <> document with the `AddAllElasticApm` or `AddElasticApm` method, it will integrate with the +https://learn.microsoft.com/aspnet/core/fundamentals/logging[ASP.NET Core logging infrastructure]. This means the Agent will pick up the configured logging provider and log as any other component logs. [IMPORTANT] @@ -192,17 +192,26 @@ See "<>". [[double-agent-initialization]] === An `InstanceAlreadyCreatedException` exception is thrown -In the early stage of a monitored process, the Agent might throw an `InstanceAlreadyCreatedException` exception with the following message: "The singleton APM agent has already been instantiated and can no longer be configured.", or an error log appears with the same message. This happens when you attempt to initialize the Agent multiple times, which is prohibited. Allowing multiple Agent instances per process would open up problems, like capturing events and metrics multiple times for each instance, or having multiple background threads for event serialization and transfer to the APM Server. +In the early stage of a monitored process, the Agent might throw an `InstanceAlreadyCreatedException` exception with the following message: +"The singleton APM agent has already been instantiated and can no longer be configured.", or an error log appears with the same message. This +happens when you attempt to initialize the Agent multiple times, which is prohibited. Allowing multiple Agent instances per process would open +up problems, like capturing events and metrics multiple times for each instance, or having multiple background threads for event serialization +and transfer to the APM Server. TIP: Take a look at the initialization section of the <> for more information on how agent initialization works. -As an example, this issue can happen if you call the `Elastic.Apm.Agent.Setup` method multiple times, or if you call another method on `Elastic.Apm.Agent` that implicitly initializes the agent, and then you call the `Elastic.Apm.Agent.Setup` method on the already initialized agent. +As an example, this issue can happen if you call the `Elastic.Apm.Agent.Setup` method multiple times, or if you call another method on `Elastic.Apm.Agent` +that implicitly initializes the agent, and then you call the `Elastic.Apm.Agent.Setup` method on the already initialized agent. -Another example might be when you use the Public Agent API in combination with the IIS module or the ASP.NET Core NuGet package, where you enable the agent with the `UseElasticApm` or `UseAllElasticApm` methods. Both the first call to the IIS module and the `UseElasticApm`/`UseAllElasticApm` methods internally call the `Elastic.Apm.Agent.Setup` method to initialize the agent. +Another example might be when you use the Public Agent API in combination with the IIS module or the ASP.NET Core NuGet package, where you enable +the agent with the `AddElasticApm` or `UseAllElasticApm` methods. Both the first call to the IIS module and the `AddElasticApm`/`AddllElasticApm` +methods internally call the `Elastic.Apm.Agent.Setup` method to initialize the agent. -You may use the Public Agent API with the `Elastic.Apm.Agent` class in code that can potentially execute before the IIS module initializes or the `UseElasticApm`/`UseAllElasticApm` calls execute. If that happens, those will fail, as the Agent has been implicitly initialized already. +You may use the Public Agent API with the `Elastic.Apm.Agent` class in code that can potentially execute before the IIS module initializes or +the `AddElasticApm`/`AddAllElasticApm` calls execute. If that happens, those will fail, as the Agent has been implicitly initialized already. -To prevent the `InstanceAlreadyCreatedException` in these scenarios, first use the `Elastic.Apm.Agent.IsConfigured` method to check if the agent is already initialized. After the check, you can safely use other methods in the Public Agent API. This will prevent accidental implicit agent initialization. +To prevent the `InstanceAlreadyCreatedException` in these scenarios, first use the `Elastic.Apm.Agent.IsConfigured` method to check if the agent +is already initialized. After the check, you can safely use other methods in the Public Agent API. This will prevent accidental implicit agent initialization. [float] [[legacy-asp-net-sync-context]] From 509d86bf797609f321937443559a08ec3f7daff5 Mon Sep 17 00:00:00 2001 From: "elastic-observability-automation[bot]" <180520183+elastic-observability-automation[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:12:58 +0000 Subject: [PATCH 74/77] deps: Bump updatecli version to v0.89.0 (#2528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit

    deps: Bump updatecli version

    deps(updatecli): Bump updatecli version to v0.89.0

    1 file(s) updated with "updatecli v0.89.0": * .tool-versions

    v0.89.0
    Release published on the 2024-12-09 07:58:42 +0000 UTC at the
    url
    https://github.com/updatecli/updatecli/releases/tag/v0.89.0
    
    ##
    Changes
    
    ## 🚀 Features
    
    - feat:
    display action result in Summary @olblak (#3223)
    
    ## 🐛
    Bug Fixes
    
    - fix(stash): use markdown body instead of
    html @ryancurrah (#3224)
    
    ## 🧰
    Maintenance
    
    - deps(go): bump module golang.org/x/text
    @updateclibot (#3255)
    - deps(go): bump module
    github.com/goccy/go-yaml @updateclibot (#3248)
    - deps: Bump
    Golang version to 1.23.4 @updateclibot (#3237)
    - deps(go): bump
    module github.com/getsops/sops/v3 @updateclibot (#3232)
    -
    deps(go): bump module github.com/drone/go-scm @updateclibot
    (#3220)
    
    ## Contributors
    
    @olblak,
    @ryancurrah, @updateclibot and @updateclibot[bot]
    
    GitHub Action workflow link
    ---
    Updatecli
logo

    Created automatically by Updatecli

    Options:

    Most of Updatecli configuration is done via its manifest(s).

    • If you close this pull request, Updatecli will automatically reopen it, the next time it runs.
    • If you close this pull request and delete the base branch, Updatecli will automatically recreate it, erasing all previous commits made.

    Feel free to report any issues at github.com/updatecli/updatecli.
    If you find this tool useful, do not hesitate to star our GitHub repository as a sign of appreciation, and/or to tell us directly on our chat!

    Co-authored-by: elastic-observability-automation[bot] <180520183+elastic-observability-automation[bot]@users.noreply.github.com> --- .tool-versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index 058189984..433f827ca 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -updatecli v0.88.1 \ No newline at end of file +updatecli v0.89.0 \ No newline at end of file From f7de335c0452ac27cf4e3c589e9ead7b5189058b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:29:52 +0000 Subject: [PATCH 75/77] Bump actions/attest-build-provenance from 2.0.1 to 2.1.0 in the github-actions group (#2531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/attest-build-provenance` from 2.0.1 to 2.1.0
    Release notes

    Sourced from actions/attest-build-provenance's releases.

    v2.1.0

    What's Changed

    Full Changelog: https://github.com/actions/attest-build-provenance/compare/v2.0.1...v2.1.0

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/attest-build-provenance&package-manager=github_actions&previous-version=2.0.1&new-version=2.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index e44db1a35..3bca4722c 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -35,7 +35,7 @@ jobs: run: ./build.sh pack - name: generate build provenance - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/build/output/_packages/*.nupkg" @@ -85,7 +85,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -93,12 +93,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04b0b4e3a..5ee511978 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,7 +78,7 @@ jobs: AGENT_ZIP_FILE=${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }} - name: Attest image - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 continue-on-error: true # continue for now until we see it working in action with: subject-name: ${{ env.DOCKER_IMAGE_NAME }} @@ -86,12 +86,12 @@ jobs: push-to-registry: true - name: generate build provenance (APM Agent) - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_AGENT }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_AGENT }}" - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_APM_PROFILER }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_APM_PROFILER }}" @@ -147,7 +147,7 @@ jobs: run: ./build.bat profiler-zip - name: generate build provenance (APM Profiler) - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "${{ github.workspace }}/${{ env.PREFIX_ZIP_FILE }}${{ steps.bootstrap.outputs.agent-version }}${{ env.SUFFIX_ZIP_FILE }}" From c4c0649dc18ad15fd89f9a878db8c1b80a103dab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:28:38 +0000 Subject: [PATCH 76/77] Bump docker/setup-buildx-action from 3.7.1 to 3.8.0 in the github-actions group (#2537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 1 update: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action). Updates `docker/setup-buildx-action` from 3.7.1 to 3.8.0
    Release notes

    Sourced from docker/setup-buildx-action's releases.

    v3.8.0

    Full Changelog: https://github.com/docker/setup-buildx-action/compare/v3.7.1...v3.8.0

    Commits
    • 6524bf6 Merge pull request #390 from crazy-max/buildx-cloud-latest
    • 8d5e074 chore: update generated content
    • 7199e57 make cloud prefix optional to download buildx if driver is cloud
    • db63cee Merge pull request #381 from docker/dependabot/github_actions/codecov/codecov...
    • 043ebe1 Merge pull request #389 from docker/dependabot/npm_and_yarn/docker/actions-to...
    • 686da90 chore: update generated content
    • a3d7487 Merge pull request #382 from docker/dependabot/npm_and_yarn/cross-spawn-7.0.6
    • 4dcdbce build(deps): bump @​docker/actions-toolkit from 0.39.0 to 0.48.0
    • 1a8ac74 ci: fix deprecated input for codecov-action
    • e827ebe build(deps): bump cross-spawn from 7.0.3 to 7.0.6
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/setup-buildx-action&package-manager=github_actions&previous-version=3.7.1&new-version=3.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 3bca4722c..e77ea7ef7 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -47,7 +47,7 @@ jobs: run: dotnet nuget push 'build/output/_packages/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} -s https://nuget.pkg.github.com/elastic/index.json --skip-duplicate --no-symbols - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ee511978..9257efbef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: run: .ci/deploy.sh ${{ secrets.NUGET_API_KEY }} ${{ secrets.NUGET_API_URL }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - name: Log in to the Elastic Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 From 6f81192005aab2f939d18603afa7a124c299a0db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:39:24 +0100 Subject: [PATCH 77/77] Bump docker/build-push-action from 6.10.0 to 6.11.0 in the github-actions group (#2540) Bumps the github-actions group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). --- .github/workflows/release-main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index e77ea7ef7..275dd0d67 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -72,7 +72,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9257efbef..a3b55c723 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: - name: Build and Push Profiler Docker Image id: docker-push continue-on-error: true # continue for now until we see it working in action - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 with: cache-from: type=gha cache-to: type=gha,mode=max