diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 825f73ebcc..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Bug Report -about: Identify a bug in the dd-trace-go codebase -title: "[BUG] " -labels: bug -assignees: '' ---- - - - -**Version of dd-trace-go** - - -**Describe what happened:** - - -**Describe what you expected:** - - -**Steps to reproduce the issue:** - - -**Additional environment details (Version of Go, Operating System, etc.):** diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 0000000000..37a1a9225a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,49 @@ +name: "Bug Report (Low Priority)" +description: "Create a public Bug Report. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult." +title: "[BUG]: " +labels: bug +body: + - type: input + attributes: + label: Tracer Version(s) + description: "Version(s) of the tracer affected by this bug. If you aren't using the [latest version](https://github.com/DataDog/dd-trace-go/releases) of dd-trace-go, try upgrading first to see if your issue has already been resolved." + placeholder: 1.70.0 + validations: + required: true + + - type: input + attributes: + label: Go Version(s) + description: "Version(s) of Go (`go version`) that you've encountered this bug with." + placeholder: "go version go1.23.2 darwin/arm64" + validations: + required: true + + - type: textarea + attributes: + label: Bug Report + description: Please add a clear and concise description of the bug here + validations: + required: true + + - type: textarea + attributes: + label: Reproduction Code + description: Please add code here to help us reproduce the problem + validations: + required: false + + - type: textarea + attributes: + label: Error Logs + description: "Please provide any error logs from the tracer (`DD_TRACE_DEBUG=true` can help)" + validations: + required: false + + - type: input + attributes: + label: Go Env Output + description: "Provide the output from `go env`" + placeholder: "GOARCH='arm64' ... GOVERSION='go1.23.2' ..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..c824d217bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Bug Report (High Priority) + url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:go + about: Create an expedited Bug Report via the helpdesk (no login required). This will allow us to look up your account and allows you to provide additional information in private. + - name: Feature Request (High Priority) + url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:go&tf_1260825272270=pt_apm_category_feature_request + about: Create an expedited Feature Request via the helpdesk (no login required). This helps with prioritization and allows you to provide additional information in private. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index a783542a60..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature -title: '' -labels: enhancement -assignees: '' ---- - - diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml new file mode 100644 index 0000000000..21882c53b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -0,0 +1,50 @@ +name: Feature Request (Low Priority) +description: Create a public Feature Request. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult. +title: "[FEATURE]: " +labels: enhancement +body: + - type: input + attributes: + label: Package Name + description: "If your feature request is to add instrumentation support for a package please provide the name here" + placeholder: doctrine/orm + validations: + required: false + + - type: input + attributes: + label: Package Version(s) + description: "If your feature request is to add instrumentation support for a package please provide the version you use" + placeholder: 3.3.0 + validations: + required: false + + - type: textarea + attributes: + label: Describe the feature you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: | + Please add a clear and concise description of your problem. + E.g. I'm unable to instrument my database queries... + validations: + required: false + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here + validations: + required: false diff --git a/.github/actions/add-codeowners/codeowners.sh b/.github/actions/add-codeowners/codeowners.sh new file mode 100755 index 0000000000..f9cec066d4 --- /dev/null +++ b/.github/actions/add-codeowners/codeowners.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +for file in "$@"; do + temp_file="tempfile.xml" + + # force write a new line at the end of the gotestsum-report.xml, or else + # the loop will skip the last line. + # fixes issue with a missing + echo -e "\n" >> $file + + while read p; do + # we might try to report gotestsum-report.xml multiple times, so don't + # calculate codeowners more times than we need + if [[ "$p" =~ \]*\)>||") + echo "$new_line" >> "$temp_file" + else + echo "$p" >> "$temp_file" + fi + done < $file + + mv "$temp_file" $file +done \ No newline at end of file diff --git a/.github/actions/dd-ci-upload/action.yml b/.github/actions/dd-ci-upload/action.yml index 3318728c50..c9da152c47 100644 --- a/.github/actions/dd-ci-upload/action.yml +++ b/.github/actions/dd-ci-upload/action.yml @@ -42,6 +42,10 @@ runs: curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_${{ env.DD_CI_CLI_BUILD }}" --output datadog-ci chmod +x datadog-ci + - name: Add CodeOwners to JUnit files + shell: bash + run: cd ./.github/actions/add-codeowners && ./codeowners.sh ${{ inputs.files }} + - name: Upload the JUnit files shell: bash run: | diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c272b36b58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + gh-actions-packages: + patterns: + - "*" diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index d238b55ba3..43aa4f3e2b 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -67,7 +67,7 @@ jobs: key: ${{ steps.cfg.outputs.key }} path: ${{ steps.cfg.outputs.path }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Compute cache configuration id: cfg @@ -75,13 +75,13 @@ jobs: echo "key=go-pkg-mod-${{ hashFiles('**/go.sum') }}" >> $GITHUB_OUTPUT echo "path=go_pkg_mod_cache" >> $GITHUB_OUTPUT - - uses: actions/setup-go@v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: cache: false - name: Cache Go modules id: cache - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ steps.cfg.outputs.path }} key: ${{ steps.cfg.outputs.key }} @@ -104,10 +104,10 @@ jobs: go-version: [ "1.23", "1.22" ] fail-fast: true # saving some CI time - macos runners are too long to get steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Restore Go modules cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ needs.go-mod-caching.outputs.path }} key: ${{ needs.go-mod-caching.outputs.key }} @@ -115,7 +115,7 @@ jobs: enableCrossOsArchive: true fail-on-cache-miss: true - - uses: actions/setup-go@v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: ${{ matrix.go-version }} cache: false # we manage the caching ourselves @@ -151,10 +151,10 @@ jobs: matrix: runs-on: [ macos-latest, windows-latest, ubuntu-latest-16-cores ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Restore Go modules cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ needs.go-mod-caching.outputs.path }} key: ${{ needs.go-mod-caching.outputs.key }} @@ -162,7 +162,7 @@ jobs: enableCrossOsArchive: true fail-on-cache-miss: true - - uses: actions/setup-go@v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: stable cache: false # we manage the caching ourselves @@ -199,10 +199,10 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Restore Go modules cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ needs.go-mod-caching.outputs.path }} key: ${{ needs.go-mod-caching.outputs.key }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 53fb2d4cc2..28ea8ff621 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,13 +35,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -52,7 +52,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 diff --git a/.github/workflows/datadog-static-analysis.yml b/.github/workflows/datadog-static-analysis.yml index 9a00adaad1..13ce59e524 100644 --- a/.github/workflows/datadog-static-analysis.yml +++ b/.github/workflows/datadog-static-analysis.yml @@ -12,7 +12,7 @@ jobs: name: Datadog Static Analyzer steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Check code meets quality and security standards id: datadog-static-analysis uses: DataDog/datadog-static-analyzer-github-action@v1 diff --git a/.github/workflows/ecosystems-label-issue.yml b/.github/workflows/ecosystems-label-issue.yml index 29853e45bc..9ba77cd4f9 100644 --- a/.github/workflows/ecosystems-label-issue.yml +++ b/.github/workflows/ecosystems-label-issue.yml @@ -15,6 +15,6 @@ jobs: steps: # https://github.com/marketplace/actions/actions-ecosystem-add-labels - name: add label - uses: actions-ecosystem/action-add-labels@v1 + uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3 with: labels: apm:ecosystem diff --git a/.github/workflows/ecosystems-label-pr.yml b/.github/workflows/ecosystems-label-pr.yml index 4cadafd3e7..da40054b3c 100644 --- a/.github/workflows/ecosystems-label-pr.yml +++ b/.github/workflows/ecosystems-label-pr.yml @@ -16,6 +16,6 @@ jobs: steps: # https://github.com/marketplace/actions/actions-ecosystem-add-labels - name: add label - uses: actions-ecosystem/action-add-labels@v1 + uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3 with: labels: apm:ecosystem diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index e7549a71fc..045c8aca95 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -24,11 +24,11 @@ jobs: govulncheck-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Checkout Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: 'stable' - name: Install govulncheck @@ -40,4 +40,4 @@ jobs: run: | go list -f '{{.Dir}}' ./contrib/... | while read dir ; do govulncheck -C $dir . - done \ No newline at end of file + done diff --git a/.github/workflows/multios-unit-tests.yml b/.github/workflows/multios-unit-tests.yml index 1cdd9191b6..3bbdac4729 100644 --- a/.github/workflows/multios-unit-tests.yml +++ b/.github/workflows/multios-unit-tests.yml @@ -40,10 +40,10 @@ jobs: DD_APPSEC_WAF_TIMEOUT: 1h steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 with: ref: ${{ inputs.ref || github.ref }} - - uses: actions/setup-go@v3 + - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/needs-triage.yml b/.github/workflows/needs-triage.yml index 6b0f1c0c44..0d78f115f3 100644 --- a/.github/workflows/needs-triage.yml +++ b/.github/workflows/needs-triage.yml @@ -94,7 +94,7 @@ jobs: steps: - name: Notify about ${{ matrix.number }} - uses: slackapi/slack-github-action@v1.24.0 + uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 with: payload: |- { diff --git a/.github/workflows/orchestrion.yml b/.github/workflows/orchestrion.yml index f5a345673f..b7d439e7bf 100644 --- a/.github/workflows/orchestrion.yml +++ b/.github/workflows/orchestrion.yml @@ -35,9 +35,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: stable cache: true @@ -54,12 +54,12 @@ jobs: json: ${{ steps.matrix.outputs.json }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ inputs.orchestrion-version != '' && 'DataDog/dd-trace-go' || github.repository }} - ref: ${{ inputs.orchestrion-version != '' && 'romain.marcadier/APPSEC-55160/orchestrion' || github.sha }} # TODO: Change to `main` in workflow_call case + ref: ${{ inputs.orchestrion-version != '' && 'main' || github.sha }} - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: stable cache: true @@ -93,16 +93,16 @@ jobs: runs-on: ${{ matrix.runs-on == 'ubuntu' && fromJson('{"labels":"ubuntu-16-core-latest","group":"Large Runner Shared Public"}') || (matrix.runs-on == 'windows' && fromJson('{"labels":"windows-shared-8core","group":"LARGE WINDOWS SHARED"}')) || format('{0}-latest', matrix.runs-on) }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: ${{ github.workspace }}/dd-trace-go repository: ${{ inputs.orchestrion-version != '' && 'DataDog/dd-trace-go' || github.repository }} - ref: ${{ inputs.orchestrion-version != '' && 'romain.marcadier/APPSEC-55160/orchestrion' || github.sha }} # TODO: Change to `main` in workflow_call case + ref: ${{ inputs.orchestrion-version != '' && 'main' || github.sha }} # If we're in workflow_dispatch/call, maybe we need to up/downgrade orchestrion - name: Check out orchestrion if: inputs.orchestrion-version != '' id: checkout-orchestrion - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: ${{ github.workspace }}/orchestrion repository: DataDog/orchestrion @@ -110,7 +110,7 @@ jobs: - name: Setup Go id: setup-go - uses: actions/setup-go@v5 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: ${{ matrix.go-version }} cache: true @@ -120,7 +120,7 @@ jobs: # ddapm-test-agent is used to observe side effects from the tracer during integration tests. - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: 3.x cache: pip @@ -214,7 +214,7 @@ jobs: echo "version=$(echo '${{ steps.setup-go.outputs.go-version }}' | cut -d'.' -f1,2)" >> "${GITHUB_OUTPUT}" - name: Upload coverage report if: inputs.collect-coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: coverage-integration+${{ matrix.mode }}+go${{ steps.go.outputs.version }}+${{ runner.os }}+${{ runner.arch }} path: ${{ github.workspace }}/orchestrion/coverage/integration.out diff --git a/.github/workflows/outdated-integrations.yml b/.github/workflows/outdated-integrations.yml index 0e559c8039..695ef9b570 100644 --- a/.github/workflows/outdated-integrations.yml +++ b/.github/workflows/outdated-integrations.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: go get github.com/Masterminds/semver/v3 @@ -30,7 +30,7 @@ jobs: - name: Create Pull Request id: pr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} branch: "upgrade-latest-major-version" diff --git a/.github/workflows/parametric-tests.yml b/.github/workflows/parametric-tests.yml index 4027451800..0df7edbef2 100644 --- a/.github/workflows/parametric-tests.yml +++ b/.github/workflows/parametric-tests.yml @@ -34,18 +34,18 @@ jobs: TEST_LIBRARY: golang steps: - name: Checkout system tests - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: repository: 'DataDog/system-tests' ref: ${{ inputs.ref }} - name: Checkout dd-trace-go - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.branch_ref || github.ref }} path: 'binaries/dd-trace-go' - - uses: actions/setup-go@v3 + - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: "oldstable" diff --git a/.github/workflows/service-extensions-publish.yml b/.github/workflows/service-extensions-publish.yml index bc1d34c8f5..3ee2d0572d 100644 --- a/.github/workflows/service-extensions-publish.yml +++ b/.github/workflows/service-extensions-publish.yml @@ -3,16 +3,15 @@ name: Publish Service Extensions Callout images packages on: push: tags: - - 'v*.*' + - 'v[0-9]+.[0-9]+.[0-9]+' workflow_dispatch: inputs: tag_name: - description: 'Docker image tag to use for the package' - required: true - default: 'dev' + description: 'Docker image tag to use for the package (default to selected branch name)' + required: false commit_sha: - description: 'Commit SHA to checkout' - required: true + description: 'Commit SHA to checkout (default to latest commit on selected branch)' + required: false set_as_latest: description: 'Set the tag as latest' required: false @@ -23,9 +22,8 @@ permissions: packages: write env: - TAG_NAME: ${{ github.ref_name || github.event.inputs.tag_name }} - REF_NAME: ${{ github.ref || github.event.inputs.commit_sha }} - COMMIT_SHA: ${{ github.sha || github.event.inputs.commit_sha }} + TAG_NAME: ${{ github.event.inputs.tag_name || github.ref_name }} + COMMIT_SHA: ${{ github.event.inputs.commit_sha || github.sha }} PUSH_LATEST: ${{ github.event.inputs.set_as_latest || 'true' }} REGISTRY_IMAGE: ghcr.io/datadog/dd-trace-go/service-extensions-callout @@ -45,7 +43,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ env.REF_NAME }} + ref: ${{ env.COMMIT_SHA }} - name: Install Docker (only arm64) if: matrix.platform == 'linux/arm64' @@ -59,7 +57,7 @@ jobs: sudo chmod 666 /var/run/docker.sock - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.8.0 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - name: Login to Docker shell: bash @@ -67,13 +65,13 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 with: images: ${{ env.REGISTRY_IMAGE }} - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0 with: context: . file: ./contrib/envoyproxy/go-control-plane/cmd/serviceextensions/Dockerfile @@ -88,7 +86,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* @@ -102,14 +100,14 @@ jobs: steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.8.0 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - name: Login to Docker shell: bash @@ -119,9 +117,13 @@ jobs: id: tags run: | tagname=${TAG_NAME//\//-} # remove slashes from tag name - echo "tags=-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${tagname} \ - -t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ env.COMMIT_SHA }} \ - ${{ env.PUSH_LATEST == 'true' && '-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest' }}" >> $GITHUB_OUTPUT + tags="tags=-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${tagname} \ + -t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ env.COMMIT_SHA }}" + if [ "${PUSH_LATEST}" == "true" ]; then + tags="$tags -t ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest" + fi + + echo $tags >> $GITHUB_OUTPUT - name: Create manifest list and push working-directory: /tmp/digests diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 9a05bdc8ba..a3a7074d3f 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -42,7 +42,7 @@ jobs: env: PACKAGES: ./internal/... ./ddtrace/... ./profiler/... ./appsec/... steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.ref || github.ref }} # Manually specify the repository, which is necessary in the workflow_call situation, as the default is @@ -50,7 +50,7 @@ jobs: # repository where the called workflow is (i.e, this repository); but I don't know of a more elegant way to # obtain its name than hard-coding it. repository: DataDog/dd-trace-go - - uses: actions/setup-go@v3 + - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: "stable" cache: true @@ -82,10 +82,10 @@ jobs: # without having to download a newer one. GOTOOLCHAIN: local steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.ref || github.ref }} - - uses: actions/setup-go@v3 + - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: "1.22" cache: true @@ -169,7 +169,7 @@ jobs: deployment-env: scratch steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.ref || github.ref }} # Manually specify the repository, which is necessary in the workflow_call situation, as the default is @@ -177,9 +177,9 @@ jobs: # repository where the called workflow is (i.e, this repository); but I don't know of a more elegant way to # obtain its name than hard-coding it. repository: DataDog/dd-trace-go - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - name: Build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0 with: context: . file: ./internal/setup-smoke-test/Dockerfile diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 24ffaae4da..23ef548d29 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: stale-pr-message: 'This PR is stale because it has been open 20 days with no activity. Remove stale label or comment or this will be closed in 10 days.' close-pr-message: 'This PR was closed because it has been open for 30 days with no activity.' diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index 3f6bcec808..8737be24af 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -108,6 +108,9 @@ jobs: weblog-variant: graphql-go - scenario: GRAPHQL_APPSEC weblog-variant: gqlgen + # Service Extensions System Tests + - scenario: EXTERNAL_PROCESSING + - scenario: EXTERNAL_PROCESSING_BLOCKING fail-fast: false env: @@ -121,13 +124,13 @@ jobs: name: Test (${{ matrix.weblog-variant }}, ${{ matrix.scenario }}) steps: - name: Checkout system tests - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: repository: 'DataDog/system-tests' ref: ${{ inputs.ref }} - name: Checkout dd-trace-go - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.branch_ref || github.ref }} path: 'binaries/dd-trace-go' @@ -149,7 +152,7 @@ jobs: run: tar -czvf artifact.tar.gz $(ls | grep logs) - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ always() }} with: name: logs_${{ matrix.weblog-variant }}_${{ matrix.scenario }} diff --git a/.github/workflows/test-apps.yml b/.github/workflows/test-apps.yml index bff3c60c53..57fba63a43 100644 --- a/.github/workflows/test-apps.yml +++ b/.github/workflows/test-apps.yml @@ -73,7 +73,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''unit-of-work/v1'') && inputs[''env: prod'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -82,7 +82,7 @@ jobs: api_key: ${{ secrets['DD_TEST_APP_API_KEY'] }} datadog_site: datadoghq.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -99,7 +99,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''unit-of-work/v1'') && inputs[''env: staging'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -108,7 +108,7 @@ jobs: api_key: ${{ secrets['DD_TEST_AND_DEMO_API_KEY'] }} datadog_site: datad0g.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -125,7 +125,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''unit-of-work/v2'') && inputs[''env: prod'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -134,7 +134,7 @@ jobs: api_key: ${{ secrets['DD_TEST_APP_API_KEY'] }} datadog_site: datadoghq.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -151,7 +151,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''unit-of-work/v2'') && inputs[''env: staging'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -160,7 +160,7 @@ jobs: api_key: ${{ secrets['DD_TEST_AND_DEMO_API_KEY'] }} datadog_site: datad0g.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -177,7 +177,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/goroutine'') && inputs[''env: prod'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -186,7 +186,7 @@ jobs: api_key: ${{ secrets['DD_TEST_APP_API_KEY'] }} datadog_site: datadoghq.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -203,7 +203,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/goroutine'') && inputs[''env: staging'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -212,7 +212,7 @@ jobs: api_key: ${{ secrets['DD_TEST_AND_DEMO_API_KEY'] }} datadog_site: datad0g.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -229,7 +229,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/heap'') && inputs[''env: prod'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -238,7 +238,7 @@ jobs: api_key: ${{ secrets['DD_TEST_APP_API_KEY'] }} datadog_site: datadoghq.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -255,7 +255,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/heap'') && inputs[''env: staging'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -264,7 +264,7 @@ jobs: api_key: ${{ secrets['DD_TEST_AND_DEMO_API_KEY'] }} datadog_site: datad0g.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -281,7 +281,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/goroutine-heap'') && inputs[''env: prod'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -290,7 +290,7 @@ jobs: api_key: ${{ secrets['DD_TEST_APP_API_KEY'] }} datadog_site: datadoghq.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true @@ -307,7 +307,7 @@ jobs: if: 'contains(fromJSON(inputs[''scenarios'']), ''memory-leak/goroutine-heap'') && inputs[''env: staging'']' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Start Agent @@ -316,7 +316,7 @@ jobs: api_key: ${{ secrets['DD_TEST_AND_DEMO_API_KEY'] }} datadog_site: datad0g.com - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: go-version: stable check-latest: true diff --git a/.github/workflows/unit-integration-tests.yml b/.github/workflows/unit-integration-tests.yml index 7594e86163..ed3ca30049 100644 --- a/.github/workflows/unit-integration-tests.yml +++ b/.github/workflows/unit-integration-tests.yml @@ -28,11 +28,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: stable - name: Copyright @@ -44,26 +44,26 @@ jobs: group: "APM Larger Runners" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} - name: golangci-lint - uses: reviewdog/action-golangci-lint@v2 + uses: reviewdog/action-golangci-lint@dd3fda91790ca90e75049e5c767509dc0ec7d99b # v2.7.0 with: golangci_lint_flags: "--timeout 10m" # We are hitting timeout when there is no cache go_version: ${{ inputs.go-version }} - golangci_lint_version: v1.59.1 - fail_on_error: true + golangci_lint_version: v1.63.4 + fail_level: error reporter: github-pr-review - name: golangci-lint (internal/orchestrion/_integration) - uses: reviewdog/action-golangci-lint@v2 + uses: reviewdog/action-golangci-lint@dd3fda91790ca90e75049e5c767509dc0ec7d99b # v2.7.0 with: golangci_lint_flags: "--timeout 10m" # We are hitting timeout when there is no cache go_version: ${{ inputs.go-version }} - golangci_lint_version: v1.59.1 - fail_on_error: true + golangci_lint_version: v1.63.4 + fail_level: error reporter: github-pr-review workdir: internal/orchestrion/_integration @@ -144,6 +144,12 @@ jobs: image: redis:3.2 ports: - 6379:6379 + valkey: + image: valkey/valkey:8 + env: + VALKEY_EXTRA_FLAGS: "--port 6380 --requirepass password-for-default" + ports: + - 6380:6380 elasticsearch2: image: elasticsearch:2 env: @@ -211,7 +217,7 @@ jobs: - 4566:4566 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} @@ -299,7 +305,7 @@ jobs: - 8126:8126 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref || github.ref }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c6d7819a1d..7292c8fee1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: INDEX_FILE: index.txt KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: dd-trace-go FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true" - BENCHMARK_TARGETS: "BenchmarkStartRequestSpan|BenchmarkHttpServeTrace|BenchmarkTracerAddSpans|BenchmarkStartSpan|BenchmarkSingleSpanRetention|BenchmarkOTelApiWithCustomTags|BenchmarkInjectW3C|BenchmarkExtractW3C|BenchmarkPartialFlushing|BenchmarkGraphQL|BenchmarkSampleWAFContext|BenchmarkCaptureStackTrace|BenchmarkSetTagString|BenchmarkSetTagStringPtr|BenchmarkSetTagMetric|BenchmarkSetTagStringer" + BENCHMARK_TARGETS: "BenchmarkStartRequestSpan|BenchmarkHttpServeTrace|BenchmarkTracerAddSpans|BenchmarkStartSpan|BenchmarkSingleSpanRetention|BenchmarkOTelApiWithCustomTags|BenchmarkInjectW3C|BenchmarkExtractW3C|BenchmarkPartialFlushing|BenchmarkGraphQL|BenchmarkSampleWAFContext|BenchmarkCaptureStackTrace|BenchmarkSetTagString|BenchmarkSetTagStringPtr|BenchmarkSetTagMetric|BenchmarkSetTagStringer|BenchmarkSerializeSpanLinksInMeta" include: - ".gitlab/benchmarks.yml" diff --git a/.gitlab/macrobenchmarks.yml b/.gitlab/macrobenchmarks.yml index ba625ee23e..a728b3ce64 100644 --- a/.gitlab/macrobenchmarks.yml +++ b/.gitlab/macrobenchmarks.yml @@ -1,7 +1,7 @@ variables: - BENCHMARKS_CI_IMAGE: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/benchmarking-platform:go-go-prof-app + BENCHMARKS_CI_IMAGE: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/benchmarking-platform:go-go-prof-app-and-serviceextensions-0001 -.benchmarks: +.benchmarks-default: stage: macrobenchmarks needs: [] tags: ["runner:apm-k8s-same-cpu"] @@ -57,12 +57,12 @@ variables: # .go123-benchmarks: - extends: .benchmarks + extends: .benchmarks-default variables: GO_VERSION: "1.23.0" .go122-benchmarks: - extends: .benchmarks + extends: .benchmarks-default variables: GO_VERSION: "1.22.5" @@ -176,3 +176,84 @@ go123-profile-trace-asm: ENABLE_PROFILING: "true" ENABLE_APPSEC: "true" DD_PROFILING_EXECUTION_TRACE_ENABLED: "false" + +# +# Macro benchmarks for Service Extensions +# (using Envoy External Processing) +# + +.benchmarks-serviceextensions: + stage: macrobenchmarks + needs: [] + tags: ["runner:apm-k8s-same-cpu"] + timeout: 1h + rules: + - if: $CI_COMMIT_REF_NAME == "main" + when: always + - when: manual + # If you have a problem with Gitlab cache, see Troubleshooting section in Benchmarking Platform docs + image: $BENCHMARKS_CI_IMAGE + script: + - git clone --branch go/go-prof-app https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/benchmarking-platform platform && cd platform + - bp-runner bp-runner.envoy_serviceextension.yml --debug + artifacts: + name: "artifacts" + when: always + paths: + - platform/artifacts-se/ + expire_in: 3 months + variables: + FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true" # Important tweak for stability of benchmarks + GO_VERSION: "1.23.0" + ARTIFACTS_DIR: "./artifacts-se" + + # Workaround: Currently we're not running the benchmarks on every PR, but GitHub still shows them as pending. + # By marking the benchmarks as allow_failure, this should go away. (This workaround should be removed once the + # benchmarks get changed to run on every PR) + allow_failure: true + + retry: + max: 2 + when: + - unknown_failure + - data_integrity_failure + - runner_system_failure + - scheduler_failure + - api_failure + +# Scenario with external processor, webserver without tracer +se-ext_proc-appsec: + extends: .benchmarks-serviceextensions + variables: + EXT_PROC: true + ENABLE_APPSEC: true + TRACER: false + +se-ext_proc-only-tracing: + extends: .benchmarks-serviceextensions + variables: + EXT_PROC: true + ENABLE_APPSEC: false + TRACER: false + +# Scenarios without external processor, webserver with tracer +se-tracer-no-ext_proc-appsec: + extends: .benchmarks-serviceextensions + variables: + EXT_PROC: false + ENABLE_APPSEC: true + TRACER: true + +se-tracer-no-ext_proc-only-tracing: + extends: .benchmarks-serviceextensions + variables: + EXT_PROC: false + ENABLE_APPSEC: false + TRACER: true + +# Scenario without tracer, only direct connection through envoy to the webserver +se-no-tracer-no-ext_proc: + extends: .benchmarks-serviceextensions + variables: + EXT_PROC: false + TRACER: false \ No newline at end of file diff --git a/contrib/database/sql/metrics.go b/contrib/database/sql/metrics.go index d8ff4ed266..5d662ec9c6 100644 --- a/contrib/database/sql/metrics.go +++ b/contrib/database/sql/metrics.go @@ -33,20 +33,27 @@ var interval = 10 * time.Second // pollDBStats calls (*DB).Stats on the db at a predetermined interval. It pushes the DBStats off to the statsd client. // the caller should always ensure that db & statsd are non-nil -func pollDBStats(statsd internal.StatsdClient, db *sql.DB) { +func pollDBStats(statsd internal.StatsdClient, db *sql.DB, stop chan struct{}) { log.Debug("DB stats will be gathered and sent every %v.", interval) - for range time.NewTicker(interval).C { - log.Debug("Reporting DB.Stats metrics...") - stat := db.Stats() - statsd.Gauge(MaxOpenConnections, float64(stat.MaxOpenConnections), []string{}, 1) - statsd.Gauge(OpenConnections, float64(stat.OpenConnections), []string{}, 1) - statsd.Gauge(InUse, float64(stat.InUse), []string{}, 1) - statsd.Gauge(Idle, float64(stat.Idle), []string{}, 1) - statsd.Gauge(WaitCount, float64(stat.WaitCount), []string{}, 1) - statsd.Timing(WaitDuration, stat.WaitDuration, []string{}, 1) - statsd.Gauge(MaxIdleClosed, float64(stat.MaxIdleClosed), []string{}, 1) - statsd.Gauge(MaxIdleTimeClosed, float64(stat.MaxIdleTimeClosed), []string{}, 1) - statsd.Gauge(MaxLifetimeClosed, float64(stat.MaxLifetimeClosed), []string{}, 1) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + log.Debug("Reporting DB.Stats metrics...") + stat := db.Stats() + statsd.Gauge(MaxOpenConnections, float64(stat.MaxOpenConnections), []string{}, 1) + statsd.Gauge(OpenConnections, float64(stat.OpenConnections), []string{}, 1) + statsd.Gauge(InUse, float64(stat.InUse), []string{}, 1) + statsd.Gauge(Idle, float64(stat.Idle), []string{}, 1) + statsd.Gauge(WaitCount, float64(stat.WaitCount), []string{}, 1) + statsd.Timing(WaitDuration, stat.WaitDuration, []string{}, 1) + statsd.Gauge(MaxIdleClosed, float64(stat.MaxIdleClosed), []string{}, 1) + statsd.Gauge(MaxIdleTimeClosed, float64(stat.MaxIdleTimeClosed), []string{}, 1) + statsd.Gauge(MaxLifetimeClosed, float64(stat.MaxLifetimeClosed), []string{}, 1) + case <-stop: + return + } } } diff --git a/contrib/database/sql/metrics_test.go b/contrib/database/sql/metrics_test.go index e68e968229..fdc69fe6f7 100644 --- a/contrib/database/sql/metrics_test.go +++ b/contrib/database/sql/metrics_test.go @@ -6,9 +6,13 @@ package sql import ( + "sync" "testing" + "github.com/DataDog/datadog-go/v5/statsd" + "github.com/lib/pq" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" ) @@ -64,3 +68,22 @@ func TestStatsTags(t *testing.T) { }) resetGlobalConfig() } + +func TestPollDBStatsStop(t *testing.T) { + driverName := "postgres" + Register(driverName, &pq.Driver{}, WithServiceName("postgres-test"), WithAnalyticsRate(0.2)) + defer unregister(driverName) + db, err := Open(driverName, "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + require.NoError(t, err) + defer db.Close() + + var wg sync.WaitGroup + stop := make(chan struct{}) + wg.Add(1) + go func() { + defer wg.Done() + pollDBStats(&statsd.NoOpClientDirect{}, db, stop) + }() + close(stop) + wg.Wait() +} diff --git a/contrib/database/sql/sql.go b/contrib/database/sql/sql.go index b26318d0d3..e20601a41d 100644 --- a/contrib/database/sql/sql.go +++ b/contrib/database/sql/sql.go @@ -139,6 +139,7 @@ type tracedConnector struct { connector driver.Connector driverName string cfg *config + dbClose chan struct{} } func (t *tracedConnector) Connect(ctx context.Context) (driver.Conn, error) { @@ -171,6 +172,13 @@ func (t *tracedConnector) Driver() driver.Driver { return t.connector.Driver() } +// Close closes the dbClose channel +// This method will be invoked when DB.Close() is called, which we expect to occur only once: https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/database/sql/sql.go;l=918-950 +func (t *tracedConnector) Close() error { + close(t.dbClose) + return nil +} + // from Go stdlib implementation of sql.Open type dsnConnector struct { dsn string @@ -208,10 +216,11 @@ func OpenDB(c driver.Connector, opts ...Option) *sql.DB { connector: c, driverName: driverName, cfg: cfg, + dbClose: make(chan struct{}), } db := sql.OpenDB(tc) if cfg.dbStats && cfg.statsdClient != nil { - go pollDBStats(cfg.statsdClient, db) + go pollDBStats(cfg.statsdClient, db, tc.dbClose) } return db } diff --git a/contrib/database/sql/sql_test.go b/contrib/database/sql/sql_test.go index 5b50b7effc..e4d587b8ea 100644 --- a/contrib/database/sql/sql_test.go +++ b/contrib/database/sql/sql_test.go @@ -281,12 +281,13 @@ func TestOpenOptions(t *testing.T) { var tg statsdtest.TestStatsdClient Register(driverName, &pq.Driver{}) defer unregister(driverName) - _, err := Open(driverName, dsn, withStatsdClient(&tg), WithDBStats()) + db, err := Open(driverName, dsn, withStatsdClient(&tg), WithDBStats()) require.NoError(t, err) // The polling interval has been reduced to 500ms for the sake of this test, so at least one round of `pollDBStats` should be complete in 1s deadline := time.Now().Add(1 * time.Second) wantStats := []string{MaxOpenConnections, OpenConnections, InUse, Idle, WaitCount, WaitDuration, MaxIdleClosed, MaxIdleTimeClosed, MaxLifetimeClosed} + var calls1 []string for { if time.Now().After(deadline) { t.Fatalf("Stats not collected in expected interval of %v", interval) @@ -300,11 +301,16 @@ func TestOpenOptions(t *testing.T) { } } // all expected stats have been collected; exit out of loop, test should pass + calls1 = calls break } // not all stats have been collected yet, try again in 50ms time.Sleep(50 * time.Millisecond) } + // Close DB and assert the no further stats have been collected; db.Close should stop the pollDBStats goroutine. + db.Close() + time.Sleep(50 * time.Millisecond) + assert.Equal(t, calls1, tg.CallNames()) }) } diff --git a/contrib/emicklei/go-restful.v3/restful.go b/contrib/emicklei/go-restful.v3/restful.go index 10fa7dce4e..e78758861f 100644 --- a/contrib/emicklei/go-restful.v3/restful.go +++ b/contrib/emicklei/go-restful.v3/restful.go @@ -46,9 +46,9 @@ func FilterFunc(configOpts ...Option) restful.FilterFunction { spanOpts = append(spanOpts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } spanOpts = append(spanOpts, httptrace.HeaderTagsFromRequest(req.Request, cfg.headerTags)) - span, ctx := httptrace.StartRequestSpan(req.Request, spanOpts...) + _, ctx, finishSpans := httptrace.StartRequestSpan(req.Request, spanOpts...) defer func() { - httptrace.FinishRequestSpan(span, resp.StatusCode(), nil, tracer.WithError(resp.Error())) + finishSpans(resp.StatusCode(), nil, tracer.WithError(resp.Error())) }() // pass the span through the request context diff --git a/contrib/emicklei/go-restful/restful.go b/contrib/emicklei/go-restful/restful.go index 8dc6ff6fc2..d49f5fd92a 100644 --- a/contrib/emicklei/go-restful/restful.go +++ b/contrib/emicklei/go-restful/restful.go @@ -46,9 +46,9 @@ func FilterFunc(configOpts ...Option) restful.FilterFunction { spanOpts = append(spanOpts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } spanOpts = append(spanOpts, httptrace.HeaderTagsFromRequest(req.Request, cfg.headerTags)) - span, ctx := httptrace.StartRequestSpan(req.Request, spanOpts...) + _, ctx, finishSpans := httptrace.StartRequestSpan(req.Request, spanOpts...) defer func() { - httptrace.FinishRequestSpan(span, resp.StatusCode(), nil, tracer.WithError(resp.Error())) + finishSpans(resp.StatusCode(), nil, tracer.WithError(resp.Error())) }() // pass the span through the request context @@ -59,9 +59,9 @@ func FilterFunc(configOpts ...Option) restful.FilterFunction { // Filter is deprecated. Please use FilterFunc. func Filter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { - span, ctx := httptrace.StartRequestSpan(req.Request, tracer.ResourceName(req.SelectedRoutePath())) + _, ctx, finishSpans := httptrace.StartRequestSpan(req.Request, tracer.ResourceName(req.SelectedRoutePath())) defer func() { - httptrace.FinishRequestSpan(span, resp.StatusCode(), nil, tracer.WithError(resp.Error())) + finishSpans(resp.StatusCode(), nil, tracer.WithError(resp.Error())) }() // pass the span through the request context diff --git a/contrib/envoyproxy/go-control-plane/envoy_test.go b/contrib/envoyproxy/go-control-plane/envoy_test.go index 7a7ac6129f..36afe49c80 100644 --- a/contrib/envoyproxy/go-control-plane/envoy_test.go +++ b/contrib/envoyproxy/go-control-plane/envoy_test.go @@ -23,6 +23,7 @@ import ( v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" ) func TestAppSec(t *testing.T) { @@ -273,6 +274,45 @@ func TestGeneratedSpan(t *testing.T) { require.Equal(t, "server", span.Tag("span.kind")) require.Equal(t, "Mistake Not...", span.Tag("http.useragent")) }) + + t.Run("span-with-injected-context", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + + // add metadata to the context + ctx = metadata.AppendToOutgoingContext(ctx, + "x-datadog-trace-id", "12345", + "x-datadog-parent-id", "67890", + ) + + stream, err := client.Process(ctx) + require.NoError(t, err) + + end2EndStreamRequest(t, stream, "/../../../resource-span/.?id=test", "GET", map[string]string{"user-agent": "Mistake Not...", "test-key": "test-value"}, map[string]string{"response-test-key": "response-test-value"}, false) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + + // Check for tags + span := finished[0] + require.Equal(t, "http.request", span.OperationName()) + require.Equal(t, "https://datadoghq.com/../../../resource-span/.?id=test", span.Tag("http.url")) + require.Equal(t, "GET", span.Tag("http.method")) + require.Equal(t, "datadoghq.com", span.Tag("http.host")) + require.Equal(t, "GET /resource-span", span.Tag("resource.name")) + require.Equal(t, "server", span.Tag("span.kind")) + require.Equal(t, "Mistake Not...", span.Tag("http.useragent")) + + // Check for trace context + require.Equal(t, uint64(12345), span.Context().TraceID()) + require.Equal(t, uint64(67890), span.ParentID()) + }) } func TestXForwardedForHeaderClientIp(t *testing.T) { diff --git a/contrib/envoyproxy/go-control-plane/fakehttp.go b/contrib/envoyproxy/go-control-plane/fakehttp.go index 3f20725e1b..62f2243be9 100644 --- a/contrib/envoyproxy/go-control-plane/fakehttp.go +++ b/contrib/envoyproxy/go-control-plane/fakehttp.go @@ -76,6 +76,28 @@ func splitPseudoHeaders(receivedHeaders []*corev3.HeaderValue) (headers map[stri return headers, pseudoHeaders } +// mergeMetadataHeaders merges the metadata headers of the grpc connection into the http headers of the request +// - Skip pseudo headers and headers that are already set +// - Set headers keys to be canonical +func mergeMetadataHeaders(md metadata.MD, headers http.Header) { + for k, v := range md { + if strings.HasPrefix(k, ":") { + continue + } + + // Skip the content-type header of the grpc request + // Note: all envoy set headers are lower-case + if k == "content-type" { + continue + } + + k = http.CanonicalHeaderKey(k) + if _, ok := headers[k]; !ok { + headers[k] = v + } + } +} + func createFakeResponseWriter(w http.ResponseWriter, res *extproc.ProcessingRequest_ResponseHeaders) error { headers, pseudoHeaders := splitPseudoHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) @@ -103,6 +125,13 @@ func newRequest(ctx context.Context, req *extproc.ProcessingRequest_RequestHeade return nil, err } + var remoteAddr string + md, ok := metadata.FromIncomingContext(ctx) + if ok { + mergeMetadataHeaders(md, headers) + remoteAddr = getRemoteAddr(md) + } + parsedURL, err := url.Parse(fmt.Sprintf("%s://%s%s", pseudoHeaders[":scheme"], pseudoHeaders[":authority"], pseudoHeaders[":path"])) if err != nil { return nil, fmt.Errorf( @@ -113,12 +142,6 @@ func newRequest(ctx context.Context, req *extproc.ProcessingRequest_RequestHeade err) } - var remoteAddr string - md, ok := metadata.FromIncomingContext(ctx) - if ok { - remoteAddr = getRemoteAddr(md) - } - var tlsState *tls.ConnectionState if pseudoHeaders[":scheme"] == "https" { tlsState = &tls.ConnectionState{} diff --git a/contrib/gin-gonic/gin/gintrace.go b/contrib/gin-gonic/gin/gintrace.go index 7a36dc1522..d338184387 100644 --- a/contrib/gin-gonic/gin/gintrace.go +++ b/contrib/gin-gonic/gin/gintrace.go @@ -52,9 +52,9 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { } opts = append(opts, tracer.Tag(ext.HTTPRoute, c.FullPath())) opts = append(opts, httptrace.HeaderTagsFromRequest(c.Request, cfg.headerTags)) - span, ctx := httptrace.StartRequestSpan(c.Request, opts...) + span, ctx, finishSpans := httptrace.StartRequestSpan(c.Request, opts...) defer func() { - httptrace.FinishRequestSpan(span, c.Writer.Status(), nil) + finishSpans(c.Writer.Status(), nil) }() // pass the span through the request context diff --git a/contrib/go-chi/chi.v5/chi.go b/contrib/go-chi/chi.v5/chi.go index 73706d9765..f1c51bb019 100644 --- a/contrib/go-chi/chi.v5/chi.go +++ b/contrib/go-chi/chi.v5/chi.go @@ -51,11 +51,11 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler { opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } opts = append(opts, httptrace.HeaderTagsFromRequest(r, cfg.headerTags)) - span, ctx := httptrace.StartRequestSpan(r, opts...) + span, ctx, finishSpans := httptrace.StartRequestSpan(r, opts...) ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) defer func() { status := ww.Status() - httptrace.FinishRequestSpan(span, status, cfg.isStatusError) + finishSpans(status, cfg.isStatusError) }() // pass the span through the request context diff --git a/contrib/go-chi/chi/chi.go b/contrib/go-chi/chi/chi.go index 1a5c2791c4..c532a2fe65 100644 --- a/contrib/go-chi/chi/chi.go +++ b/contrib/go-chi/chi/chi.go @@ -51,11 +51,11 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler { opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } opts = append(opts, httptrace.HeaderTagsFromRequest(r, cfg.headerTags)) - span, ctx := httptrace.StartRequestSpan(r, opts...) + span, ctx, finishSpans := httptrace.StartRequestSpan(r, opts...) ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) defer func() { status := ww.Status() - httptrace.FinishRequestSpan(span, status, cfg.isStatusError) + finishSpans(status, cfg.isStatusError) }() // pass the span through the request context diff --git a/contrib/go-redis/redis.v7/redis.go b/contrib/go-redis/redis.v7/redis.go index 5376ae5e9c..4c003bebd3 100644 --- a/contrib/go-redis/redis.v7/redis.go +++ b/contrib/go-redis/redis.v7/redis.go @@ -79,7 +79,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } else { @@ -91,7 +91,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } diff --git a/contrib/go-redis/redis.v8/redis.go b/contrib/go-redis/redis.v8/redis.go index a3ec7bc1c0..aa3ba4e6e7 100644 --- a/contrib/go-redis/redis.v8/redis.go +++ b/contrib/go-redis/redis.v8/redis.go @@ -78,7 +78,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } else { @@ -90,7 +90,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } diff --git a/contrib/go-redis/redis/redis.go b/contrib/go-redis/redis/redis.go index 56feb6dcfd..9e40db15ba 100644 --- a/contrib/go-redis/redis/redis.go +++ b/contrib/go-redis/redis/redis.go @@ -130,7 +130,7 @@ func (c *Pipeliner) execWithContext(ctx context.Context) ([]redis.Cmder, error) tracer.ResourceName("redis"), tracer.Tag(ext.TargetHost, p.host), tracer.Tag(ext.TargetPort, p.port), - tracer.Tag("out.db", strconv.Itoa(p.db)), + tracer.Tag(ext.TargetDB, strconv.Itoa(p.db)), tracer.Tag(ext.Component, componentName), tracer.Tag(ext.SpanKind, ext.SpanKindClient), tracer.Tag(ext.DBSystem, ext.DBSystemRedis), @@ -202,7 +202,7 @@ func createWrapperFromClient(tc *Client) func(oldProcess func(cmd redis.Cmder) e tracer.ResourceName(parts[0]), tracer.Tag(ext.TargetHost, p.host), tracer.Tag(ext.TargetPort, p.port), - tracer.Tag("out.db", strconv.Itoa(p.db)), + tracer.Tag(ext.TargetDB, strconv.Itoa(p.db)), tracer.Tag("redis.raw_command", raw), tracer.Tag("redis.args_length", strconv.Itoa(length)), tracer.Tag(ext.Component, componentName), diff --git a/contrib/gorm.io/gorm.v1/orchestrion.yml b/contrib/gorm.io/gorm.v1/orchestrion.yml index bb0eac6e12..c1ab640b2b 100644 --- a/contrib/gorm.io/gorm.v1/orchestrion.yml +++ b/contrib/gorm.io/gorm.v1/orchestrion.yml @@ -13,4 +13,18 @@ aspects: join-point: function-call: gorm.io/gorm.Open advice: - - replace-function: gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1.Open + - wrap-expression: + imports: + gorm: gorm.io/gorm + gormtrace: gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1 + template: |- + func() (*gorm.DB, error) { + db, err := {{ . }} + if err != nil { + return nil, err + } + if err := db.Use(gormtrace.NewTracePlugin()); err != nil { + return nil, err + } + return db, nil + }() diff --git a/contrib/internal/httptrace/before_handle.go b/contrib/internal/httptrace/before_handle.go index f3be037d4b..d791f6a9cc 100644 --- a/contrib/internal/httptrace/before_handle.go +++ b/contrib/internal/httptrace/before_handle.go @@ -57,11 +57,11 @@ func BeforeHandle(cfg *ServeConfig, w http.ResponseWriter, r *http.Request) (htt if cfg.Route != "" { opts = append(opts, tracer.Tag(ext.HTTPRoute, cfg.Route)) } - span, ctx := StartRequestSpan(r, opts...) + span, ctx, finishSpans := StartRequestSpan(r, opts...) rw, ddrw := wrapResponseWriter(w) rt := r.WithContext(ctx) closeSpan := func() { - FinishRequestSpan(span, ddrw.status, cfg.IsStatusError, cfg.FinishOpts...) + finishSpans(ddrw.status, cfg.IsStatusError, cfg.FinishOpts...) } afterHandle := closeSpan handled := false diff --git a/contrib/internal/httptrace/config.go b/contrib/internal/httptrace/config.go index 70e94d1c40..12bcb0ce2f 100644 --- a/contrib/internal/httptrace/config.go +++ b/contrib/internal/httptrace/config.go @@ -26,16 +26,19 @@ const ( envTraceClientIPEnabled = "DD_TRACE_CLIENT_IP_ENABLED" // envServerErrorStatuses is the name of the env var used to specify error status codes on http server spans envServerErrorStatuses = "DD_TRACE_HTTP_SERVER_ERROR_STATUSES" + // envInferredProxyServicesEnabled is the name of the env var used for enabling inferred span tracing + envInferredProxyServicesEnabled = "DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED" ) // defaultQueryStringRegexp is the regexp used for query string obfuscation if [EnvQueryStringRegexp] is empty. var defaultQueryStringRegexp = regexp.MustCompile("(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:\"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:\"|%22)(?:%2[^2]|%[^2]|[^\"%])+(?:\"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}") type config struct { - queryStringRegexp *regexp.Regexp // specifies the regexp to use for query string obfuscation. - queryString bool // reports whether the query string should be included in the URL span tag. - traceClientIP bool - isStatusError func(statusCode int) bool + queryStringRegexp *regexp.Regexp // specifies the regexp to use for query string obfuscation. + queryString bool // reports whether the query string should be included in the URL span tag. + traceClientIP bool + isStatusError func(statusCode int) bool + inferredProxyServicesEnabled bool } // ResetCfg sets local variable cfg back to its defaults (mainly useful for testing) @@ -45,10 +48,11 @@ func ResetCfg() { func newConfig() config { c := config{ - queryString: !internal.BoolEnv(envQueryStringDisabled, false), - queryStringRegexp: QueryStringRegexp(), - traceClientIP: internal.BoolEnv(envTraceClientIPEnabled, false), - isStatusError: isServerError, + queryString: !internal.BoolEnv(envQueryStringDisabled, false), + queryStringRegexp: QueryStringRegexp(), + traceClientIP: internal.BoolEnv(envTraceClientIPEnabled, false), + isStatusError: isServerError, + inferredProxyServicesEnabled: internal.BoolEnv(envInferredProxyServicesEnabled, false), } v := os.Getenv(envServerErrorStatuses) if fn := GetErrorCodesFromInput(v); fn != nil { diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index e5ad7d9d64..22ac0306b4 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -10,9 +10,12 @@ package httptrace import ( "context" "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "net/http" "strconv" "strings" + "sync" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" @@ -26,17 +29,61 @@ var ( cfg = newConfig() ) +var reportTelemetryConfigOnce sync.Once + +type inferredSpanCreatedCtxKey struct{} + +type FinishSpanFunc = func(status int, errorFn func(int) bool, opts ...tracer.FinishOption) + // StartRequestSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, // http.useragent). Any further span start option can be added with opts. -func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { +func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context, FinishSpanFunc) { // Append our span options before the given ones so that the caller can "overwrite" them. // TODO(): rework span start option handling (https://github.com/DataDog/dd-trace-go/issues/1352) + // we cannot track the configuration in newConfig because it's called during init() and the the telemetry client + // is not initialized yet + reportTelemetryConfigOnce.Do(func() { + telemetry.GlobalClient.ConfigChange([]telemetry.Configuration{ + {Name: "inferred_proxy_services_enabled", Value: cfg.inferredProxyServicesEnabled}, + }) + log.Debug("internal/httptrace: telemetry.ConfigChange called with cfg: %v:", cfg) + }) + var ipTags map[string]string if cfg.traceClientIP { ipTags, _ = httpsec.ClientIPTags(r.Header, true, r.RemoteAddr) } + nopts := make([]ddtrace.StartSpanOption, 0, len(opts)+1+len(ipTags)) + + var inferredProxySpan tracer.Span + + if cfg.inferredProxyServicesEnabled { + inferredProxySpanCreated := false + + if created, ok := r.Context().Value(inferredSpanCreatedCtxKey{}).(bool); ok { + inferredProxySpanCreated = created + } + + if !inferredProxySpanCreated { + var inferredStartSpanOpts []ddtrace.StartSpanOption + + requestProxyContext, err := extractInferredProxyContext(r.Header) + if err != nil { + log.Debug(err.Error()) + } else { + spanParentCtx, spanParentErr := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)) + if spanParentErr == nil { + if spanLinksCtx, spanLinksOk := spanParentCtx.(ddtrace.SpanContextWithLinks); spanLinksOk { + inferredStartSpanOpts = append(inferredStartSpanOpts, tracer.WithSpanLinks(spanLinksCtx.SpanLinks())) + } + } + inferredProxySpan = startInferredProxySpan(requestProxyContext, spanParentCtx, inferredStartSpanOpts...) + } + } + } + nopts = append(nopts, func(ssCfg *ddtrace.StartSpanConfig) { if ssCfg.Tags == nil { @@ -50,19 +97,38 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. if r.Host != "" { ssCfg.Tags["http.host"] = r.Host } - if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil { - // If there are span links as a result of context extraction, add them as a StartSpanOption - if linksCtx, ok := spanctx.(ddtrace.SpanContextWithLinks); ok && linksCtx.SpanLinks() != nil { - tracer.WithSpanLinks(linksCtx.SpanLinks())(ssCfg) + + if inferredProxySpan != nil { + tracer.ChildOf(inferredProxySpan.Context())(ssCfg) + } else { + spanParentCtx, spanParentErr := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)) + if spanParentErr == nil { + if spanLinksCtx, spanLinksOk := spanParentCtx.(ddtrace.SpanContextWithLinks); spanLinksOk { + tracer.WithSpanLinks(spanLinksCtx.SpanLinks())(ssCfg) + } + tracer.ChildOf(spanParentCtx)(ssCfg) } - tracer.ChildOf(spanctx)(ssCfg) } + for k, v := range ipTags { ssCfg.Tags[k] = v } }) + nopts = append(nopts, opts...) - return tracer.StartSpanFromContext(r.Context(), namingschema.OpName(namingschema.HTTPServer), nopts...) + + var requestContext = r.Context() + if inferredProxySpan != nil { + requestContext = context.WithValue(r.Context(), inferredSpanCreatedCtxKey{}, true) + } + + span, ctx := tracer.StartSpanFromContext(requestContext, namingschema.OpName(namingschema.HTTPServer), nopts...) + return span, ctx, func(status int, errorFn func(int) bool, opts ...tracer.FinishOption) { + FinishRequestSpan(span, status, errorFn, opts...) + if inferredProxySpan != nil { + FinishRequestSpan(inferredProxySpan, status, errorFn, opts...) + } + } } // FinishRequestSpan finishes the given HTTP request span and sets the expected response-related tags such as the status diff --git a/contrib/internal/httptrace/httptrace_api_gateway_test.go b/contrib/internal/httptrace/httptrace_api_gateway_test.go new file mode 100644 index 0000000000..b9f866ebe0 --- /dev/null +++ b/contrib/internal/httptrace/httptrace_api_gateway_test.go @@ -0,0 +1,194 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package httptrace + +import ( + "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer" +) + +func TestInferredProxySpans(t *testing.T) { + t.Setenv("DD_SERVICE", "aws-server") + t.Setenv("DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED", "true") + ResetCfg() + + startTime := time.Now().Add(-5 * time.Second) + + inferredHeaders := map[string]string{ + "x-dd-proxy": "aws-apigateway", + "x-dd-proxy-request-time-ms": strconv.FormatInt(startTime.UnixMilli(), 10), + "x-dd-proxy-path": "/test", + "x-dd-proxy-httpmethod": "GET", + "x-dd-proxy-domain-name": "example.com", + "x-dd-proxy-stage": "dev", + } + srvURL := "https://example.com/test" + + t.Run("should create parent and child spans for a 200", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/", srvURL), nil) + require.NoError(t, err) + + for k, v := range inferredHeaders { + req.Header.Set(k, v) + } + + _, _, finishSpans := StartRequestSpan(req) + finishSpans(200, nil) + + spans := mt.FinishedSpans() + require.Equal(t, 2, len(spans)) + + gwSpan := spans[1] + webReqSpan := spans[0] + assert.Equal(t, "aws.apigateway", gwSpan.OperationName()) + assert.Equal(t, "http.request", webReqSpan.OperationName()) + assert.Equal(t, "example.com", gwSpan.Tag("service.name")) + assert.True(t, webReqSpan.ParentID() == gwSpan.SpanID()) + assert.Equal(t, webReqSpan.Tag("http.status_code"), gwSpan.Tag("http.status_code")) + assert.Equal(t, webReqSpan.Tag("span.type"), gwSpan.Tag("span.type")) + + assert.Equal(t, startTime.UnixMilli(), gwSpan.StartTime().UnixMilli()) + + for _, arg := range inferredHeaders { + header, tag := normalizer.HeaderTag(arg) + + // Default to an empty string if the tag does not exist + gwSpanTags, exists := gwSpan.Tags()[tag] + if !exists { + gwSpanTags = "" + } + expectedTags := strings.Join(req.Header.Values(header), ",") + assert.Equal(t, expectedTags, gwSpanTags) + } + }) + + t.Run("should create parent and child spans for error", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/error", srvURL), nil) + require.NoError(t, err) + + for k, v := range inferredHeaders { + req.Header.Set(k, v) + } + + _, _, finishSpans := StartRequestSpan(req) + finishSpans(500, nil) + + spans := mt.FinishedSpans() + require.Equal(t, 2, len(spans)) + + gwSpan := spans[1] + webReqSpan := spans[0] + assert.Equal(t, "aws.apigateway", gwSpan.OperationName()) + assert.Equal(t, "http.request", webReqSpan.OperationName()) + assert.Equal(t, "example.com", gwSpan.Tag("service.name")) + assert.True(t, webReqSpan.ParentID() == gwSpan.SpanID()) + assert.Equal(t, webReqSpan.Tag("http.status_code"), gwSpan.Tag("http.status_code")) + assert.Equal(t, webReqSpan.Tag("span.type"), gwSpan.Tag("span.type")) + assert.Equal(t, startTime.UnixMilli(), gwSpan.StartTime().UnixMilli()) + + assert.Equal(t, "500: Internal Server Error", gwSpan.Tag(ext.Error).(error).Error()) + assert.Equal(t, "500: Internal Server Error", webReqSpan.Tag(ext.Error).(error).Error()) + + for _, arg := range inferredHeaders { + header, tag := normalizer.HeaderTag(arg) + + // Default to an empty string if the tag does not exist + gwSpanTags, exists := gwSpan.Tags()[tag] + if !exists { + gwSpanTags = "" + } + expectedTags := strings.Join(req.Header.Values(header), ",") + assert.Equal(t, expectedTags, gwSpanTags) + } + }) + + t.Run("should not create API Gateway span if headers are missing", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/no-aws-headers", srvURL), nil) + require.NoError(t, err) + + _, _, finishSpans := StartRequestSpan(req) + finishSpans(200, nil) + + assert.Equal(t, http.StatusOK, 200) + + spans := mt.FinishedSpans() + require.Equal(t, 1, len(spans)) + assert.Equal(t, "http.request", spans[0].OperationName()) + }) + + t.Run("should not create API Gateway span if x-dd-proxy is missing", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/no-aws-headers", srvURL), nil) + require.NoError(t, err) + + for k, v := range inferredHeaders { + if k != "x-dd-proxy" { + req.Header.Set(k, v) + } + } + + _, _, finishSpans := StartRequestSpan(req) + finishSpans(200, nil) + + spans := mt.FinishedSpans() + assert.Equal(t, 1, len(spans)) + assert.Equal(t, "http.request", spans[0].OperationName()) + }) + + t.Run("should not create more than one API Gateway span for a local trace", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/", srvURL), nil) + require.NoError(t, err) + for k, v := range inferredHeaders { + req.Header.Set(k, v) + } + + _, ctx, finishSpans1 := StartRequestSpan(req) + finishSpans1(200, nil) + + req2 := req.WithContext(ctx) + _, _, finishSpans2 := StartRequestSpan(req2) + finishSpans2(200, nil) + + spans := mt.FinishedSpans() + require.Equal(t, 3, len(spans)) + + gwSpan := spans[1] + webReqSpan := spans[0] + assert.Equal(t, "aws.apigateway", gwSpan.OperationName()) + assert.Equal(t, "http.request", webReqSpan.OperationName()) + assert.Equal(t, "example.com", gwSpan.Tag("service.name")) + assert.True(t, webReqSpan.ParentID() == gwSpan.SpanID()) + assert.Equal(t, webReqSpan.Tag("http.status_code"), gwSpan.Tag("http.status_code")) + assert.Equal(t, webReqSpan.Tag("span.type"), gwSpan.Tag("span.type")) + + assert.Equal(t, startTime.UnixMilli(), gwSpan.StartTime().UnixMilli()) + }) +} diff --git a/contrib/internal/httptrace/httptrace_test.go b/contrib/internal/httptrace/httptrace_test.go index 4dbd98d793..1f389228c7 100644 --- a/contrib/internal/httptrace/httptrace_test.go +++ b/contrib/internal/httptrace/httptrace_test.go @@ -97,7 +97,7 @@ func TestConfiguredErrorStatuses(t *testing.T) { statuses := []int{0, 200, 400, 500} r := httptest.NewRequest(http.MethodGet, "/test", nil) for i, status := range statuses { - sp, _ := StartRequestSpan(r) + sp, _, _ := StartRequestSpan(r) FinishRequestSpan(sp, status, nil) spans := mt.FinishedSpans() require.Len(t, spans, i+1) @@ -125,7 +125,7 @@ func TestConfiguredErrorStatuses(t *testing.T) { ResetCfg() r := httptest.NewRequest(http.MethodGet, "/test", nil) - sp, _ := StartRequestSpan(r) + sp, _, _ := StartRequestSpan(r) FinishRequestSpan(sp, 0, nil) spans := mt.FinishedSpans() require.Len(t, spans, 1) @@ -153,7 +153,7 @@ func TestHeaderTagsFromRequest(t *testing.T) { hs := []string{"header1:tag1", "header2:tag2", "header3:tag3", "x-datadog-header:tag4"} ht := internal.NewLockMap(normalizer.HeaderTagSlice(hs)) - s, _ := StartRequestSpan(r, HeaderTagsFromRequest(r, ht)) + s, _, _ := StartRequestSpan(r, HeaderTagsFromRequest(r, ht)) s.Finish() spans := mt.FinishedSpans() require.Len(t, spans, 1) @@ -167,7 +167,7 @@ func TestStartRequestSpan(t *testing.T) { mt := mocktracer.Start() defer mt.Stop() r := httptest.NewRequest(http.MethodGet, "/somePath", nil) - s, _ := StartRequestSpan(r) + s, _, _ := StartRequestSpan(r) s.Finish() spans := mt.FinishedSpans() require.Len(t, spans, 1) @@ -237,7 +237,7 @@ func TestTraceClientIPFlag(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/somePath", nil) r.RemoteAddr = tc.remoteAddr - s, _ := StartRequestSpan(r) + s, _, _ := StartRequestSpan(r) s.Finish() spans := mt.FinishedSpans() targetSpan := spans[0] diff --git a/contrib/internal/httptrace/inferred_proxy.go b/contrib/internal/httptrace/inferred_proxy.go new file mode 100644 index 0000000000..8781d8e8e9 --- /dev/null +++ b/contrib/internal/httptrace/inferred_proxy.go @@ -0,0 +1,152 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package httptrace + +import ( + "errors" + "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "net/http" + "strconv" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +// These constants are intended to be used by tracers to extract and infer +// parent span information for distributed tracing systems. +const ( + // ProxyHeaderSystem is the header used to indicate the source of the + // proxy. In the case of AWS API Gateway, the value of this header + // will always be 'aws-apigateway'. + ProxyHeaderSystem = "X-Dd-Proxy" + + // ProxyHeaderStartTimeMs is the header used to indicate the start time + // of the request in milliseconds. This value corresponds to the + // 'context.requestTimeEpoch' in AWS API Gateway, providing a timestamp + // for when the request was initiated. + ProxyHeaderStartTimeMs = "X-Dd-Proxy-Request-Time-Ms" + + // ProxyHeaderPath is the header used to indicate the path of the + // request. This value corresponds to 'context.path' in AWS API Gateway, + // and helps identify the resource that the request is targeting. + ProxyHeaderPath = "X-Dd-Proxy-Path" + + // ProxyHeaderHttpMethod is the header used to indicate the HTTP method + // of the request (e.g., GET, POST, PUT, DELETE). This value corresponds + // to 'context.httpMethod' in AWS API Gateway, and provides the method + // used to make the request. + ProxyHeaderHttpMethod = "X-Dd-Proxy-Httpmethod" + + // ProxyHeaderDomain is the header used to indicate the AWS domain name + // handling the request. This value corresponds to 'context.domainName' + // in AWS API Gateway, which represents the custom domain associated + // with the API Gateway. + ProxyHeaderDomain = "X-Dd-Proxy-Domain-Name" + + // ProxyHeaderStage is the header used to indicate the AWS stage name + // for the API request. This value corresponds to 'context.stage' in + // AWS API Gateway, and provides the stage (e.g., dev, prod, etc.) + // in which the request is being processed. + ProxyHeaderStage = "X-Dd-Proxy-Stage" +) + +type proxyDetails struct { + spanName string + component string +} + +type proxyContext struct { + startTime time.Time + method string + path string + stage string + domainName string + proxySystemName string +} + +var ( + supportedProxies = map[string]proxyDetails{ + "aws-apigateway": { + spanName: "aws.apigateway", + component: "aws-apigateway", + }, + } +) + +func extractInferredProxyContext(headers http.Header) (*proxyContext, error) { + _, exists := headers[ProxyHeaderStartTimeMs] + if !exists { + return nil, errors.New("proxy header start time does not exist") + } + + proxyHeaderSystem, exists := headers[ProxyHeaderSystem] + if !exists { + return nil, errors.New("proxy header system does not exist") + } + + if _, ok := supportedProxies[proxyHeaderSystem[0]]; !ok { + return nil, errors.New("unsupported Proxy header system") + } + + pc := proxyContext{ + method: headers.Get(ProxyHeaderHttpMethod), + path: headers.Get(ProxyHeaderPath), + stage: headers.Get(ProxyHeaderStage), + domainName: headers.Get(ProxyHeaderDomain), + proxySystemName: headers.Get(ProxyHeaderSystem), + } + + startTimeUnixMilli, err := strconv.ParseInt(headers[ProxyHeaderStartTimeMs][0], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing time string: %v", err) + } + pc.startTime = time.UnixMilli(startTimeUnixMilli) + + return &pc, nil +} + +func startInferredProxySpan(requestProxyContext *proxyContext, parent ddtrace.SpanContext, opts ...ddtrace.StartSpanOption) tracer.Span { + proxySpanInfo := supportedProxies[requestProxyContext.proxySystemName] + log.Debug(`Successfully extracted inferred span info ${proxyContext} for proxy: ${proxyContext.proxySystemName}`) + + startTime := requestProxyContext.startTime + + configService := requestProxyContext.domainName + if configService == "" { + configService = globalconfig.ServiceName() + } + + optsLocal := make([]ddtrace.StartSpanOption, len(opts), len(opts)+1) + copy(optsLocal, opts) + + optsLocal = append(optsLocal, + func(cfg *ddtrace.StartSpanConfig) { + if cfg.Tags == nil { + cfg.Tags = make(map[string]interface{}) + } + + cfg.Parent = parent + cfg.StartTime = startTime + + cfg.Tags[ext.SpanType] = ext.SpanTypeWeb + cfg.Tags[ext.ServiceName] = configService + cfg.Tags[ext.Component] = proxySpanInfo.component + cfg.Tags[ext.HTTPMethod] = requestProxyContext.method + cfg.Tags[ext.HTTPURL] = requestProxyContext.domainName + requestProxyContext.path + cfg.Tags[ext.HTTPRoute] = requestProxyContext.path + cfg.Tags[ext.ResourceName] = fmt.Sprintf("%s %s", requestProxyContext.method, requestProxyContext.path) + cfg.Tags["stage"] = requestProxyContext.stage + }, + ) + + span := tracer.StartSpan(proxySpanInfo.spanName, optsLocal...) + + return span +} diff --git a/contrib/internal/telemetrytest/telemetry_test.go b/contrib/internal/telemetrytest/telemetry_test.go index 8e2f130343..123b2a73ca 100644 --- a/contrib/internal/telemetrytest/telemetry_test.go +++ b/contrib/internal/telemetrytest/telemetry_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "os" "os/exec" + "path/filepath" "strings" "testing" @@ -87,8 +88,9 @@ func TestTelemetryEnabled(t *testing.T) { if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") || strings.Contains(pkg.ImportPath, "/cmd") { continue } - p := strings.Replace(pkg.Dir, pkg.Root, "../..", 1) - if strings.Contains(p, "/contrib/net/http/client") || strings.Contains(p, "/contrib/os") { + sep := string(os.PathSeparator) + p := strings.Replace(pkg.Dir, pkg.Root, filepath.Join("..", ".."), 1) + if strings.Contains(p, filepath.Join(sep, "contrib", "net", "http", "client")) || strings.Contains(p, filepath.Join(sep, "contrib", "os")) { continue } if !pkg.hasTelemetryImport(t) { diff --git a/contrib/jackc/pgx.v5/metrics.go b/contrib/jackc/pgx.v5/metrics.go index eb94c50bfc..9b84a4553f 100644 --- a/contrib/jackc/pgx.v5/metrics.go +++ b/contrib/jackc/pgx.v5/metrics.go @@ -35,6 +35,7 @@ var interval = 10 * time.Second // pollPoolStats calls (*pgxpool).Stats on the pool at a predetermined interval. It pushes the pool Stats off to the statsd client. func pollPoolStats(statsd internal.StatsdClient, pool *pgxpool.Pool) { + // TODO: Create stop condition for pgx on db.Close log.Debug("contrib/jackc/pgx.v5: Traced pool connection found: Pool stats will be gathered and sent every %v.", interval) for range time.NewTicker(interval).C { log.Debug("contrib/jackc/pgx.v5: Reporting pgxpool.Stat metrics...") diff --git a/contrib/labstack/echo.v4/echotrace.go b/contrib/labstack/echo.v4/echotrace.go index d3a8867bd4..fab1d5f7c5 100644 --- a/contrib/labstack/echo.v4/echotrace.go +++ b/contrib/labstack/echo.v4/echotrace.go @@ -10,7 +10,6 @@ import ( "fmt" "math" "net/http" - "strconv" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/options" @@ -48,6 +47,7 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { tracer.Tag(ext.Component, componentName), tracer.Tag(ext.SpanKind, ext.SpanKindServer), ) + return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // If we have an ignoreRequestFunc, use it to see if we proceed with tracing @@ -72,10 +72,7 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { finishOpts = []tracer.FinishOption{tracer.NoDebugStack()} } - span, ctx := httptrace.StartRequestSpan(request, opts...) - defer func() { - span.Finish(finishOpts...) - }() + span, ctx, finishSpans := httptrace.StartRequestSpan(request, opts...) // pass the span through the request context c.SetRequest(request.WithContext(ctx)) @@ -85,6 +82,7 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { } // serve the request to the next middleware err := next(c) + var echoStatus int if err != nil && !shouldIgnoreError(cfg, err) { // It is impossible to determine what the final status code of a request is in echo. // This is the best we can do. @@ -92,13 +90,14 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { if cfg.isStatusError(echoErr.Code) { finishOpts = append(finishOpts, tracer.WithError(err)) } - span.SetTag(ext.HTTPCode, strconv.Itoa(echoErr.Code)) + echoStatus = echoErr.Code + } else { // Any error that is not an *echo.HTTPError will be treated as an error with 500 status code. if cfg.isStatusError(500) { finishOpts = append(finishOpts, tracer.WithError(err)) } - span.SetTag(ext.HTTPCode, "500") + echoStatus = 500 } } else if status := c.Response().Status; status > 0 { if cfg.isStatusError(status) { @@ -106,15 +105,25 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { finishOpts = append(finishOpts, tracer.WithError(statusErr)) } } - span.SetTag(ext.HTTPCode, strconv.Itoa(status)) + echoStatus = status } else { if cfg.isStatusError(200) { if statusErr := errorFromStatusCode(200); !shouldIgnoreError(cfg, statusErr) { finishOpts = append(finishOpts, tracer.WithError(statusErr)) } } - span.SetTag(ext.HTTPCode, "200") + echoStatus = 200 } + defer func() { + finishSpans(echoStatus, func(status int) bool { + if cfg.isStatusError(status) { + if statusErr := errorFromStatusCode(status); !shouldIgnoreError(cfg, statusErr) { + return true + } + } + return false + }, finishOpts...) + }() return err } } diff --git a/contrib/labstack/echo/echotrace.go b/contrib/labstack/echo/echotrace.go index f1f7f063de..d4cf943174 100644 --- a/contrib/labstack/echo/echotrace.go +++ b/contrib/labstack/echo/echotrace.go @@ -14,7 +14,6 @@ import ( "fmt" "math" "net/http" - "strconv" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/options" @@ -65,16 +64,14 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { finishOpts = []tracer.FinishOption{tracer.NoDebugStack()} } - span, ctx := httptrace.StartRequestSpan(request, opts...) - defer func() { - span.Finish(finishOpts...) - }() + _, ctx, finishSpans := httptrace.StartRequestSpan(request, opts...) // pass the span through the request context c.SetRequest(request.WithContext(ctx)) // serve the request to the next middleware err := next(c) + var echoStatus int if err != nil { // It is impossible to determine what the final status code of a request is in echo. // This is the best we can do. @@ -83,25 +80,28 @@ func Middleware(opts ...Option) echo.MiddlewareFunc { if cfg.isStatusError(echoErr.Code) { finishOpts = append(finishOpts, tracer.WithError(err)) } - span.SetTag(ext.HTTPCode, strconv.Itoa(echoErr.Code)) + echoStatus = echoErr.Code } else { // Any error that is not an *echo.HTTPError will be treated as an error with 500 status code. if cfg.isStatusError(500) { finishOpts = append(finishOpts, tracer.WithError(err)) } - span.SetTag(ext.HTTPCode, "500") + echoStatus = 500 } } else if status := c.Response().Status; status > 0 { if cfg.isStatusError(status) { finishOpts = append(finishOpts, tracer.WithError(fmt.Errorf("%d: %s", status, http.StatusText(status)))) } - span.SetTag(ext.HTTPCode, strconv.Itoa(status)) + echoStatus = status } else { if cfg.isStatusError(200) { finishOpts = append(finishOpts, tracer.WithError(fmt.Errorf("%d: %s", 200, http.StatusText(200)))) } - span.SetTag(ext.HTTPCode, "200") + echoStatus = 200 } + defer func() { + finishSpans(echoStatus, cfg.isStatusError, finishOpts...) + }() return err } } diff --git a/contrib/log/slog/slog.go b/contrib/log/slog/slog.go index a29866f294..ccb9800ee4 100644 --- a/contrib/log/slog/slog.go +++ b/contrib/log/slog/slog.go @@ -61,7 +61,7 @@ func (h *handler) Handle(ctx context.Context, rec slog.Record) error { // In case the user has created group loggers, we ignore those and // set them at the root level. span, ok := tracer.SpanFromContext(ctx) - if ok { + if ok && span.Context().TraceID() != 0 { traceID := strconv.FormatUint(span.Context().TraceID(), 10) spanID := strconv.FormatUint(span.Context().SpanID(), 10) diff --git a/contrib/log/slog/slog_test.go b/contrib/log/slog/slog_test.go index 7546009b0f..36a5960db4 100644 --- a/contrib/log/slog/slog_test.go +++ b/contrib/log/slog/slog_test.go @@ -48,8 +48,29 @@ func assertLogEntry(t *testing.T, rawEntry, wantMsg, wantLevel string, span trac } } +func assertLogEntryNoTrace(t *testing.T, rawEntry, wantMsg, wantLevel string) { + t.Helper() + + t.Log(rawEntry) + + var entry map[string]interface{} + err := json.Unmarshal([]byte(rawEntry), &entry) + require.NoError(t, err) + require.NotEmpty(t, entry) + + assert.Equal(t, wantMsg, entry["msg"]) + assert.Equal(t, wantLevel, entry["level"]) + assert.NotEmpty(t, entry["time"]) + + assert.NotContains(t, entry, ext.LogKeyTraceID) + assert.NotContains(t, entry, ext.LogKeySpanID) +} + func testLogger(t *testing.T, createLogger func(b io.Writer) *slog.Logger, assertExtra func(t *testing.T, entry map[string]interface{})) { - tracer.Start(tracer.WithLogger(internallog.DiscardLogger{})) + tracer.Start( + tracer.WithTraceEnabled(true), + tracer.WithLogger(internallog.DiscardLogger{}), + ) defer tracer.Stop() // create the application logger @@ -74,25 +95,50 @@ func testLogger(t *testing.T, createLogger func(b io.Writer) *slog.Logger, asser assertLogEntry(t, logs[1], "this is an error log with tracing information", "ERROR", span, assertExtra) } -func TestNewJSONHandler(t *testing.T) { - testLogger( - t, - func(w io.Writer) *slog.Logger { - return slog.New(NewJSONHandler(w, nil)) - }, - nil, +func testLoggerNoTrace(t *testing.T, createLogger func(b io.Writer) *slog.Logger) { + tracer.Start( + tracer.WithTraceEnabled(false), + tracer.WithLogger(internallog.DiscardLogger{}), + ) + defer tracer.Stop() + + // create the application logger + var b bytes.Buffer + logger := createLogger(&b) + + // start a new span + span, ctx := tracer.StartSpanFromContext(context.Background(), "test") + defer span.Finish() + + // log a message using the context containing span information + logger.Log(ctx, slog.LevelInfo, "this is an info log with tracing information") + logger.Log(ctx, slog.LevelError, "this is an error log with tracing information") + + logs := strings.Split( + strings.TrimRight(b.String(), "\n"), + "\n", ) + // assert log entries contain trace information + require.Len(t, logs, 2) + assertLogEntryNoTrace(t, logs[0], "this is an info log with tracing information", "INFO") + assertLogEntryNoTrace(t, logs[1], "this is an error log with tracing information", "ERROR") +} + +func TestNewJSONHandler(t *testing.T) { + createLogger := func(w io.Writer) *slog.Logger { + return slog.New(NewJSONHandler(w, nil)) + } + testLogger(t, createLogger, nil) + testLoggerNoTrace(t, createLogger) } func TestWrapHandler(t *testing.T) { t.Run("testLogger", func(t *testing.T) { - testLogger( - t, - func(w io.Writer) *slog.Logger { - return slog.New(WrapHandler(slog.NewJSONHandler(w, nil))) - }, - nil, - ) + createLogger := func(w io.Writer) *slog.Logger { + return slog.New(WrapHandler(slog.NewJSONHandler(w, nil))) + } + testLogger(t, createLogger, nil) + testLoggerNoTrace(t, createLogger) }) t.Run("slogtest", func(t *testing.T) { diff --git a/contrib/net/http/orchestrion.client.yml b/contrib/net/http/orchestrion.client.yml index 1d2b2d2322..9842a92761 100644 --- a/contrib/net/http/orchestrion.client.yml +++ b/contrib/net/http/orchestrion.client.yml @@ -55,6 +55,7 @@ aspects: ddtrace: gopkg.in/DataDog/dd-trace-go.v1/ddtrace os: os links: + - gopkg.in/DataDog/dd-trace-go.v1/internal - gopkg.in/DataDog/dd-trace-go.v1/internal/appsec - gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec - gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer @@ -87,6 +88,12 @@ aspects: //go:linkname __dd_httptrace_GetErrorCodesFromInput gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace.GetErrorCodesFromInput func __dd_httptrace_GetErrorCodesFromInput(string) func(int) bool + //go:linkname __dd_httptrace_UrlFromRequest gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace.UrlFromRequest + func __dd_httptrace_UrlFromRequest(*Request, bool) string + + //go:linkname __dd_internal_BoolEnv gopkg.in/DataDog/dd-trace-go.v1/internal.BoolEnv + func __dd_internal_BoolEnv(string, bool) bool + type __dd_tracer_HTTPHeadersCarrier Header func (c __dd_tracer_HTTPHeadersCarrier) Set(key, val string) { Header(c).Set(key, val) @@ -97,6 +104,8 @@ aspects: return statusCode >= 400 && statusCode < 500 } + var __dd_queryStringEnabled = __dd_internal_BoolEnv("DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING", true) + func init() { v := os.Getenv("DD_TRACE_HTTP_CLIENT_ERROR_STATUSES") if fn := __dd_httptrace_GetErrorCodesFromInput(v); fn != nil { @@ -130,7 +139,7 @@ aspects: __dd_tracer_SpanType(ext.SpanTypeHTTP), __dd_tracer_ResourceName(resourceName), __dd_tracer_Tag(ext.HTTPMethod, {{ $req }}.Method), - __dd_tracer_Tag(ext.HTTPURL, url.String()), + __dd_tracer_Tag(ext.HTTPURL, __dd_httptrace_UrlFromRequest({{ $req }}, __dd_queryStringEnabled)), __dd_tracer_Tag(ext.Component, "net/http"), __dd_tracer_Tag(ext.SpanKind, ext.SpanKindClient), __dd_tracer_Tag(ext.NetworkDestinationName, url.Hostname()), diff --git a/contrib/redis/go-redis.v9/redis.go b/contrib/redis/go-redis.v9/redis.go index 97a4fa6b10..848371533a 100644 --- a/contrib/redis/go-redis.v9/redis.go +++ b/contrib/redis/go-redis.v9/redis.go @@ -78,7 +78,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), } } else { host, port, err := net.SplitHostPort(opt.Addr) @@ -89,7 +89,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), } } } else if clientOptions, ok := client.(clusterOptions); ok { diff --git a/contrib/urfave/negroni/negroni.go b/contrib/urfave/negroni/negroni.go index 651188df1a..c8e6fcdf61 100644 --- a/contrib/urfave/negroni/negroni.go +++ b/contrib/urfave/negroni/negroni.go @@ -42,7 +42,7 @@ func (m *DatadogMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, ne if !math.IsNaN(m.cfg.analyticsRate) { opts = append(opts, tracer.Tag(ext.EventSampleRate, m.cfg.analyticsRate)) } - span, ctx := httptrace.StartRequestSpan(r, opts...) + _, ctx, finishSpans := httptrace.StartRequestSpan(r, opts...) defer func() { // check if the responseWriter is of type negroni.ResponseWriter var ( @@ -56,7 +56,7 @@ func (m *DatadogMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, ne opts = []tracer.FinishOption{tracer.WithError(fmt.Errorf("%d: %s", status, http.StatusText(status)))} } } - httptrace.FinishRequestSpan(span, status, m.cfg.isStatusError, opts...) + finishSpans(status, m.cfg.isStatusError, opts...) }() next(w, r.WithContext(ctx)) diff --git a/contrib/valkey-go/example_test.go b/contrib/valkey-go/example_test.go new file mode 100644 index 0000000000..236a35c91c --- /dev/null +++ b/contrib/valkey-go/example_test.go @@ -0,0 +1,35 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package valkey_test + +import ( + "context" + "log" + + "github.com/valkey-io/valkey-go" + valkeytrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/valkey-go" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// To start tracing Valkey, simply create a new client using the library and continue +// using as you normally would. +func Example() { + tracer.Start() + defer tracer.Stop() + + vk, err := valkeytrace.NewClient(valkey.ClientOption{ + InitAddress: []string{"localhost:6379"}, + }) + if err != nil { + log.Fatal(err) + return + } + + if err := vk.Do(context.Background(), vk.B().Set().Key("key").Value("value").Build()).Error(); err != nil { + log.Fatal(err) + return + } +} diff --git a/contrib/valkey-go/option.go b/contrib/valkey-go/option.go new file mode 100644 index 0000000000..60766ef45a --- /dev/null +++ b/contrib/valkey-go/option.go @@ -0,0 +1,42 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package valkey + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema" +) + +type config struct { + rawCommand bool + serviceName string +} + +// Option represents an option that can be used to create or wrap a client. +type Option func(*config) + +func defaultConfig() *config { + return &config{ + // Do not include the raw command by default since it could contain sensitive data. + rawCommand: internal.BoolEnv("DD_TRACE_VALKEY_RAW_COMMAND", false), + serviceName: namingschema.ServiceName(defaultServiceName), + } +} + +// WithRawCommand can be used to set a tag `valkey.raw_command` in the created spans (disabled by default). +// Warning: please note the datadog-agent currently does not support obfuscation for this tag, so use this at your own risk. +func WithRawCommand(rawCommand bool) Option { + return func(cfg *config) { + cfg.rawCommand = rawCommand + } +} + +// WithServiceName sets the given service name for the client. +func WithServiceName(name string) Option { + return func(cfg *config) { + cfg.serviceName = name + } +} diff --git a/contrib/valkey-go/valkey.go b/contrib/valkey-go/valkey.go new file mode 100644 index 0000000000..5362b397e7 --- /dev/null +++ b/contrib/valkey-go/valkey.go @@ -0,0 +1,284 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Package valkey provides tracing functions for tracing the valkey-io/valkey-go package (https://github.com/valkey-io/valkey-go). +package valkey + +import ( + "context" + "net" + "strconv" + "strings" + "time" + + "github.com/valkey-io/valkey-go" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" +) + +const ( + componentName = "valkey-io/valkey-go" + defaultServiceName = "valkey.client" +) + +func init() { + telemetry.LoadIntegration(componentName) + tracer.MarkIntegrationImported("github.com/valkey-io/valkey-go") +} + +var ( + _ valkey.Client = (*client)(nil) +) + +type client struct { + client valkey.Client + cfg *config + host string + port string + dbIndex string + user string +} + +func (c *client) B() valkey.Builder { + return c.client.B() +} + +func (c *client) Close() { + c.client.Close() +} + +// NewClient returns a new valkey.Client enhanced with tracing. +func NewClient(clientOption valkey.ClientOption, opts ...Option) (valkey.Client, error) { + valkeyClient, err := valkey.NewClient(clientOption) + if err != nil { + return nil, err + } + cfg := defaultConfig() + for _, fn := range opts { + fn(cfg) + } + tClient := &client{ + client: valkeyClient, + cfg: cfg, + dbIndex: strconv.FormatInt(int64(clientOption.SelectDB), 10), + user: clientOption.Username, + } + if len(clientOption.InitAddress) > 0 { + host, port, err := net.SplitHostPort(clientOption.InitAddress[0]) + if err == nil { + tClient.host = host + tClient.port = port + } + } + return tClient, nil +} + +func (c *client) Do(ctx context.Context, cmd valkey.Completed) valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp := c.client.Do(ctx, cmd) + setClientCacheTags(span, resp) + span.Finish(tracer.WithError(resp.Error())) + return resp +} + +func (c *client) DoMulti(ctx context.Context, multi ...valkey.Completed) []valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommandMulti(multi)) + resp := c.client.DoMulti(ctx, multi...) + c.finishSpan(span, firstError(resp)) + return resp +} + +func (c *client) Receive(ctx context.Context, subscribe valkey.Completed, fn func(msg valkey.PubSubMessage)) error { + span, ctx := c.startSpan(ctx, processCommand(&subscribe)) + err := c.client.Receive(ctx, subscribe, fn) + c.finishSpan(span, err) + return err +} + +func (c *client) DoCache(ctx context.Context, cmd valkey.Cacheable, ttl time.Duration) valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp := c.client.DoCache(ctx, cmd, ttl) + setClientCacheTags(span, resp) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) DoMultiCache(ctx context.Context, multi ...valkey.CacheableTTL) []valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommandMultiCache(multi)) + resp := c.client.DoMultiCache(ctx, multi...) + c.finishSpan(span, firstError(resp)) + return resp +} + +func (c *client) DoStream(ctx context.Context, cmd valkey.Completed) (resp valkey.ValkeyResultStream) { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp = c.client.DoStream(ctx, cmd) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) DoMultiStream(ctx context.Context, multi ...valkey.Completed) valkey.MultiValkeyResultStream { + span, ctx := c.startSpan(ctx, processCommandMulti(multi)) + resp := c.client.DoMultiStream(ctx, multi...) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) Dedicated(fn func(valkey.DedicatedClient) error) error { + return c.client.Dedicated(func(dc valkey.DedicatedClient) error { + return fn(&dedicatedClient{ + client: c, + dedicatedClient: dc, + }) + }) +} + +func (c *client) Dedicate() (client valkey.DedicatedClient, cancel func()) { + dedicated, cancel := c.client.Dedicate() + return &dedicatedClient{ + client: c, + dedicatedClient: dedicated, + }, cancel +} + +func (c *client) Nodes() map[string]valkey.Client { + nodes := c.client.Nodes() + for addr, valkeyClient := range nodes { + host, port, _ := net.SplitHostPort(addr) + nodes[addr] = &client{ + client: valkeyClient, + cfg: c.cfg, + host: host, + port: port, + dbIndex: c.dbIndex, + user: c.user, + } + } + return nodes +} + +var ( + _ valkey.DedicatedClient = (*dedicatedClient)(nil) +) + +type dedicatedClient struct { + *client + dedicatedClient valkey.DedicatedClient +} + +func (c *dedicatedClient) SetPubSubHooks(hooks valkey.PubSubHooks) <-chan error { + return c.dedicatedClient.SetPubSubHooks(hooks) +} + +type command struct { + statement string + raw string +} + +func (c *client) startSpan(ctx context.Context, cmd command) (tracer.Span, context.Context) { + opts := []tracer.StartSpanOption{ + tracer.ServiceName(c.cfg.serviceName), + tracer.ResourceName(cmd.statement), + tracer.SpanType(ext.SpanTypeValkey), + tracer.Tag(ext.TargetHost, c.host), + tracer.Tag(ext.TargetPort, c.port), + tracer.Tag(ext.Component, componentName), + tracer.Tag(ext.SpanKind, ext.SpanKindClient), + tracer.Tag(ext.DBSystem, ext.DBSystemValkey), + tracer.Tag(ext.TargetDB, c.dbIndex), + } + if c.cfg.rawCommand { + opts = append(opts, tracer.Tag(ext.ValkeyRawCommand, cmd.raw)) + } + if c.host != "" { + opts = append(opts, tracer.Tag(ext.TargetHost, c.host)) + } + if c.port != "" { + opts = append(opts, tracer.Tag(ext.TargetPort, c.port)) + } + if c.user != "" { + opts = append(opts, tracer.Tag(ext.DBUser, c.user)) + } + return tracer.StartSpanFromContext(ctx, "valkey.command", opts...) +} + +func (c *client) finishSpan(span tracer.Span, err error) { + var opts []tracer.FinishOption + if err != nil && !valkey.IsValkeyNil(err) { + opts = append(opts, tracer.WithError(err)) + } + span.Finish(opts...) +} + +type commander interface { + Commands() []string +} + +func processCommand(cmd commander) command { + cmds := cmd.Commands() + if len(cmds) == 0 { + return command{} + } + statement := cmds[0] + raw := strings.Join(cmds, " ") + return command{ + statement: statement, + raw: raw, + } +} + +func processCommandMulti(multi []valkey.Completed) command { + var cmds []command + for _, cmd := range multi { + cmds = append(cmds, processCommand(&cmd)) + } + return multiCommand(cmds) +} + +func processCommandMultiCache(multi []valkey.CacheableTTL) command { + var cmds []command + for _, cmd := range multi { + cmds = append(cmds, processCommand(&cmd.Cmd)) + } + return multiCommand(cmds) +} + +func multiCommand(cmds []command) command { + // limit to the 5 first + if len(cmds) > 5 { + cmds = cmds[:5] + } + statement := strings.Builder{} + raw := strings.Builder{} + for i, cmd := range cmds { + statement.WriteString(cmd.statement) + raw.WriteString(cmd.raw) + if i != len(cmds)-1 { + statement.WriteString(" ") + raw.WriteString(" ") + } + } + return command{ + statement: statement.String(), + raw: raw.String(), + } +} + +func firstError(s []valkey.ValkeyResult) error { + for _, result := range s { + if err := result.Error(); err != nil && !valkey.IsValkeyNil(err) { + return err + } + } + return nil +} + +func setClientCacheTags(s tracer.Span, result valkey.ValkeyResult) { + s.SetTag(ext.ValkeyClientCacheHit, result.IsCacheHit()) + s.SetTag(ext.ValkeyClientCacheTTL, result.CacheTTL()) + s.SetTag(ext.ValkeyClientCachePTTL, result.CachePTTL()) + s.SetTag(ext.ValkeyClientCachePXAT, result.CachePXAT()) +} diff --git a/contrib/valkey-go/valkey_test.go b/contrib/valkey-go/valkey_test.go new file mode 100644 index 0000000000..19799bf5d6 --- /dev/null +++ b/contrib/valkey-go/valkey_test.go @@ -0,0 +1,304 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. +package valkey + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valkey-io/valkey-go" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" +) + +const ( + // See docker-compose.yaml + valkeyPort = 6380 + valkeyUsername = "default" + valkeyPassword = "password-for-default" +) + +var ( + valkeyAddrs = []string{fmt.Sprintf("127.0.0.1:%d", valkeyPort)} +) + +func TestMain(m *testing.M) { + _, ok := os.LookupEnv("INTEGRATION") + if !ok { + fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable") + os.Exit(0) + } + os.Exit(m.Run()) +} + +func TestNewClient(t *testing.T) { + prevName := globalconfig.ServiceName() + defer globalconfig.SetServiceName(prevName) + globalconfig.SetServiceName("global-service") + + tests := []struct { + name string + opts []Option + runTest func(*testing.T, context.Context, valkey.Client) + assertSpans func(*testing.T, []mocktracer.Span) + wantServiceName string + }{ + { + name: "Test SET command with raw command", + opts: []Option{ + WithRawCommand(true), + WithServiceName("test-service"), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + assert.NoError(t, client.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "test-service", + }, + { + name: "Test SET command without raw command", + opts: nil, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + require.NoError(t, client.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Nil(t, span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test SET GET multi command", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + resp := client.DoMulti(ctx, client.B().Set().Key("test_key").Value("test_value").Build(), client.B().Get().Key("test_key").Build()) + require.Len(t, resp, 2) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET GET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value GET test_key", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test HMGET command with cache", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + assert.NoError(t, client.DoCache(ctx, client.B().Hmget().Key("mk").Field("1", "2").Cache(), time.Minute).Error()) + resp, err := client.DoCache(ctx, client.B().Hmget().Key("mk").Field("1", "2").Cache(), time.Minute).ToArray() + require.Len(t, resp, 2) + require.NoError(t, err) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 2) + + span := spans[0] + assert.Equal(t, "HMGET", span.Tag(ext.ResourceName)) + assert.Equal(t, "HMGET mk 1 2", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Greater(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + + span = spans[1] + assert.Equal(t, "HMGET", span.Tag(ext.ResourceName)) + assert.Equal(t, "HMGET mk 1 2", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, true, span.Tag(ext.ValkeyClientCacheHit)) + assert.Greater(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test GET stream command", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + resp := client.DoStream(ctx, client.B().Get().Key("test_key").Build()) + require.NoError(t, resp.Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "GET", span.Tag(ext.ResourceName)) + assert.Equal(t, "GET test_key", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test multi command should be limited to 5", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Nanosecond) + client.DoMulti( + ctxWithTimeout, + client.B().Set().Key("k1").Value("v1").Build(), + client.B().Get().Key("k1").Build(), + client.B().Set().Key("k2").Value("v2").Build(), + client.B().Get().Key("k2").Build(), + client.B().Set().Key("k3").Value("v3").Build(), + client.B().Get().Key("k3").Build(), + ) + cancel() + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET GET SET GET SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET k1 v1 GET k1 SET k2 v2 GET k2 SET k3 v3", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Equal(t, context.DeadlineExceeded, span.Tag(ext.Error).(error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test SUBSCRIBE command with timeout", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Millisecond) + require.EqualError(t, + context.DeadlineExceeded, + client.Receive(ctxWithTimeout, client.B().Subscribe().Channel("test_channel").Build(), func(msg valkey.PubSubMessage) {}).Error(), + ) + cancel() + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SUBSCRIBE", span.Tag(ext.ResourceName)) + assert.Equal(t, "SUBSCRIBE test_channel", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Equal(t, context.DeadlineExceeded, span.Tag(ext.Error).(error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test Dedicated client", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + err := client.Dedicated(func(d valkey.DedicatedClient) error { + return d.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error() + }) + require.NoError(t, err) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + valkeyClientOption := valkey.ClientOption{ + InitAddress: valkeyAddrs, + Username: valkeyUsername, + Password: valkeyPassword, + } + client, err := NewClient(valkeyClientOption, tt.opts...) + require.NoError(t, err) + + root, ctx := tracer.StartSpanFromContext(context.Background(), "test.root", tracer.ServiceName("test-service")) + tt.runTest(t, ctx, client) + root.Finish() // test.root exists in the last span. + + spans := mt.FinishedSpans() + tt.assertSpans(t, spans[:len(spans)-1]) + + for _, span := range spans { + if span.OperationName() == "test.root" { + continue + } + + // The following assertions are common to all spans + assert.Equal(t, tt.wantServiceName, span.Tag(ext.ServiceName)) + assert.Equal(t, "127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal(t, "6380", span.Tag(ext.TargetPort)) + assert.Equal(t, "0", span.Tag(ext.TargetDB)) + assert.Equal(t, "default", span.Tag(ext.DBUser)) + assert.Equal(t, "valkey.command", span.OperationName()) + assert.Equal(t, "client", span.Tag(ext.SpanKind)) + assert.Equal(t, "valkey", span.Tag(ext.SpanType)) + assert.Equal(t, "valkey-io/valkey-go", span.Tag(ext.Component)) + assert.Equal(t, "valkey", span.Tag(ext.DBSystem)) + } + }) + } + +} diff --git a/ddtrace/ddtrace.go b/ddtrace/ddtrace.go index 928816ba6c..fc645aff88 100644 --- a/ddtrace/ddtrace.go +++ b/ddtrace/ddtrace.go @@ -103,25 +103,6 @@ type SpanContext interface { ForeachBaggageItem(handler func(k, v string) bool) } -// SpanLink represents a reference to a span that exists outside of the trace. -// -//go:generate msgp -unexported -marshal=false -o=span_link_msgp.go -tests=false - -type SpanLink struct { - // TraceID represents the low 64 bits of the linked span's trace id. This field is required. - TraceID uint64 `msg:"trace_id" json:"trace_id"` - // TraceIDHigh represents the high 64 bits of the linked span's trace id. This field is only set if the linked span's trace id is 128 bits. - TraceIDHigh uint64 `msg:"trace_id_high,omitempty" json:"trace_id_high"` - // SpanID represents the linked span's span id. - SpanID uint64 `msg:"span_id" json:"span_id"` - // Attributes is a mapping of keys to string values. These values are used to add additional context to the span link. - Attributes map[string]string `msg:"attributes,omitempty" json:"attributes"` - // Tracestate is the tracestate of the linked span. This field is optional. - Tracestate string `msg:"tracestate,omitempty" json:"tracestate"` - // Flags represents the W3C trace flags of the linked span. This field is optional. - Flags uint32 `msg:"flags,omitempty" json:"flags"` -} - // StartSpanOption is a configuration option that can be used with a Tracer's StartSpan method. type StartSpanOption func(cfg *StartSpanConfig) diff --git a/ddtrace/ext/app_types.go b/ddtrace/ext/app_types.go index eb6ded8f60..1313c78a76 100644 --- a/ddtrace/ext/app_types.go +++ b/ddtrace/ext/app_types.go @@ -51,6 +51,9 @@ const ( // also have a "redis.raw_command" tag. SpanTypeRedis = "redis" + // SpanTypeRedis marks a span as a Valkey operation. + SpanTypeValkey = "valkey" + // SpanTypeMemcached marks a span as a memcached operation. SpanTypeMemcached = "memcached" diff --git a/ddtrace/ext/db.go b/ddtrace/ext/db.go index c9a046f86d..ff9b2efe33 100644 --- a/ddtrace/ext/db.go +++ b/ddtrace/ext/db.go @@ -32,6 +32,7 @@ const ( DBSystemOtherSQL = "other_sql" DBSystemElasticsearch = "elasticsearch" DBSystemRedis = "redis" + DBSystemValkey = "valkey" DBSystemMongoDB = "mongodb" DBSystemCassandra = "cassandra" DBSystemConsulKV = "consul" @@ -57,6 +58,24 @@ const ( RedisDatabaseIndex = "db.redis.database_index" ) +// Valkey tags. +const ( + // ValkeyRawCommand allows to set the raw command for tags. + ValkeyRawCommand = "valkey.raw_command" + + // ValkeyClientCacheHit is the remaining TTL in seconds of client side cache. + ValkeyClientCacheHit = "db.valkey.client.cache.hit" + + // ValkeyClientCacheTTL captures the Time-To-Live (TTL) of a cached entry in the client. + ValkeyClientCacheTTL = "db.valkey.client.cache.ttl" + + // ValkeyClientCachePTTL is the remaining PTTL in seconds of client side cache. + ValkeyClientCachePTTL = "db.valkey.client.cache.pttl" + + // ValkeyClientCachePXAT is the remaining PXAT in seconds of client side cache. + ValkeyClientCachePXAT = "db.valkey.client.cache.pxat" +) + // Cassandra tags. const ( // CassandraQuery is the tag name used for cassandra queries. diff --git a/ddtrace/ext/tags.go b/ddtrace/ext/tags.go index 375d7df7b5..b070b2fcd6 100644 --- a/ddtrace/ext/tags.go +++ b/ddtrace/ext/tags.go @@ -9,7 +9,6 @@ package ext const ( // TargetHost sets the target host address. - // Deprecated: Use NetworkDestinationName instead for hostname and NetworkDestinationIP for IP addresses TargetHost = "out.host" // NetworkDestinationName is the remote hostname or similar where the outbound connection is being made to. @@ -19,9 +18,11 @@ const ( NetworkDestinationIP = "network.destination.ip" // TargetPort sets the target host port. - // Deprecated: Use NetworkDestinationPort instead. TargetPort = "out.port" + // TargetDB sets the target db. + TargetDB = "out.db" + // NetworkDestinationPort is the remote port number of the outbound connection. NetworkDestinationPort = "network.destination.port" diff --git a/ddtrace/span_link.go b/ddtrace/span_link.go new file mode 100644 index 0000000000..9d6cca604a --- /dev/null +++ b/ddtrace/span_link.go @@ -0,0 +1,25 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package ddtrace + +// SpanLink represents a reference to a span that exists outside of the trace. +// +//go:generate msgp -unexported -marshal=false -o=span_link_msgp.go -tests=false + +type SpanLink struct { + // TraceID represents the low 64 bits of the linked span's trace id. This field is required. + TraceID uint64 `msg:"trace_id" json:"trace_id"` + // TraceIDHigh represents the high 64 bits of the linked span's trace id. This field is only set if the linked span's trace id is 128 bits. + TraceIDHigh uint64 `msg:"trace_id_high" json:"trace_id_high"` + // SpanID represents the linked span's span id. + SpanID uint64 `msg:"span_id" json:"span_id"` + // Attributes is a mapping of keys to string values. These values are used to add additional context to the span link. + Attributes map[string]string `msg:"attributes" json:"attributes"` + // Tracestate is the tracestate of the linked span. This field is optional. + Tracestate string `msg:"tracestate" json:"tracestate"` + // Flags represents the W3C trace flags of the linked span. This field is optional. + Flags uint32 `msg:"flags" json:"flags"` +} diff --git a/ddtrace/span_link_msgp.go b/ddtrace/span_link_msgp.go index c2b90ec516..d3ec8a8fb2 100644 --- a/ddtrace/span_link_msgp.go +++ b/ddtrace/span_link_msgp.go @@ -97,55 +97,27 @@ func (z *SpanLink) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *SpanLink) EncodeMsg(en *msgp.Writer) (err error) { - // omitempty: check for empty values - zb0001Len := uint32(6) - var zb0001Mask uint8 /* 6 bits */ - if z.TraceIDHigh == 0 { - zb0001Len-- - zb0001Mask |= 0x2 - } - if z.Attributes == nil { - zb0001Len-- - zb0001Mask |= 0x8 - } - if z.Tracestate == "" { - zb0001Len-- - zb0001Mask |= 0x10 - } - if z.Flags == 0 { - zb0001Len-- - zb0001Mask |= 0x20 - } - // variable map header, size zb0001Len - err = en.Append(0x80 | uint8(zb0001Len)) + // map header, size 6 + // write "trace_id" + err = en.Append(0x86, 0xa8, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64) if err != nil { return } - if zb0001Len == 0 { + err = en.WriteUint64(z.TraceID) + if err != nil { + err = msgp.WrapError(err, "TraceID") return } - // write "trace_id" - err = en.Append(0xa8, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64) + // write "trace_id_high" + err = en.Append(0xad, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x68, 0x69, 0x67, 0x68) if err != nil { return } - err = en.WriteUint64(z.TraceID) + err = en.WriteUint64(z.TraceIDHigh) if err != nil { - err = msgp.WrapError(err, "TraceID") + err = msgp.WrapError(err, "TraceIDHigh") return } - if (zb0001Mask & 0x2) == 0 { // if not empty - // write "trace_id_high" - err = en.Append(0xad, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x68, 0x69, 0x67, 0x68) - if err != nil { - return - } - err = en.WriteUint64(z.TraceIDHigh) - if err != nil { - err = msgp.WrapError(err, "TraceIDHigh") - return - } - } // write "span_id" err = en.Append(0xa7, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x69, 0x64) if err != nil { @@ -156,54 +128,48 @@ func (z *SpanLink) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "SpanID") return } - if (zb0001Mask & 0x8) == 0 { // if not empty - // write "attributes" - err = en.Append(0xaa, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73) - if err != nil { - return - } - err = en.WriteMapHeader(uint32(len(z.Attributes))) - if err != nil { - err = msgp.WrapError(err, "Attributes") - return - } - for za0001, za0002 := range z.Attributes { - err = en.WriteString(za0001) - if err != nil { - err = msgp.WrapError(err, "Attributes") - return - } - err = en.WriteString(za0002) - if err != nil { - err = msgp.WrapError(err, "Attributes", za0001) - return - } - } + // write "attributes" + err = en.Append(0xaa, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73) + if err != nil { + return } - if (zb0001Mask & 0x10) == 0 { // if not empty - // write "tracestate" - err = en.Append(0xaa, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x74, 0x61, 0x74, 0x65) - if err != nil { - return - } - err = en.WriteString(z.Tracestate) - if err != nil { - err = msgp.WrapError(err, "Tracestate") - return - } + err = en.WriteMapHeader(uint32(len(z.Attributes))) + if err != nil { + err = msgp.WrapError(err, "Attributes") + return } - if (zb0001Mask & 0x20) == 0 { // if not empty - // write "flags" - err = en.Append(0xa5, 0x66, 0x6c, 0x61, 0x67, 0x73) + for za0001, za0002 := range z.Attributes { + err = en.WriteString(za0001) if err != nil { + err = msgp.WrapError(err, "Attributes") return } - err = en.WriteUint32(z.Flags) + err = en.WriteString(za0002) if err != nil { - err = msgp.WrapError(err, "Flags") + err = msgp.WrapError(err, "Attributes", za0001) return } } + // write "tracestate" + err = en.Append(0xaa, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x74, 0x61, 0x74, 0x65) + if err != nil { + return + } + err = en.WriteString(z.Tracestate) + if err != nil { + err = msgp.WrapError(err, "Tracestate") + return + } + // write "flags" + err = en.Append(0xa5, 0x66, 0x6c, 0x61, 0x67, 0x73) + if err != nil { + return + } + err = en.WriteUint32(z.Flags) + if err != nil { + err = msgp.WrapError(err, "Flags") + return + } return } diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 1efdcd2359..a9fedd5044 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -99,6 +99,7 @@ var contribIntegrations = map[string]struct { "github.com/zenazn/goji": {"Goji", false}, "log/slog": {"log/slog", false}, "github.com/uptrace/bun": {"Bun", false}, + "github.com/valkey-io/valkey-go": {"Valkey", false}, } var ( @@ -773,6 +774,7 @@ func loadAgentFeatures(agentDisabled bool, agentURL *url.URL, httpClient *http.C features.DropP0s = info.ClientDropP0s features.StatsdPort = info.Config.StatsdPort features.metaStructAvailable = info.SpanMetaStruct + features.peerTags = info.PeerTags for _, endpoint := range info.Endpoints { switch endpoint { case "/v0.6/stats": diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 9a5c6ee9dd..9045400905 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -224,7 +224,7 @@ func TestLoadAgentFeatures(t *testing.T) { t.Run("OK", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte(`{"endpoints":["/v0.6/stats"],"feature_flags":["a","b"],"client_drop_p0s":true,"config": {"statsd_port":8999}}`)) + w.Write([]byte(`{"endpoints":["/v0.6/stats"],"feature_flags":["a","b"],"client_drop_p0s":true,"peer_tags":["peer.hostname"],"config": {"statsd_port":8999}}`)) })) defer srv.Close() cfg := newConfig(WithAgentAddr(strings.TrimPrefix(srv.URL, "http://")), WithAgentTimeout(2)) @@ -237,6 +237,7 @@ func TestLoadAgentFeatures(t *testing.T) { assert.True(t, cfg.agent.Stats) assert.True(t, cfg.agent.HasFlag("a")) assert.True(t, cfg.agent.HasFlag("b")) + assert.EqualValues(t, cfg.agent.peerTags, []string{"peer.hostname"}) }) t.Run("discovery", func(t *testing.T) { @@ -271,7 +272,7 @@ func TestAgentIntegration(t *testing.T) { defer clearIntegrationsForTests() cfg.loadContribIntegrations(nil) - assert.Equal(t, 56, len(cfg.integrations)) + assert.Equal(t, 57, len(cfg.integrations)) for integrationName, v := range cfg.integrations { assert.False(t, v.Instrumented, "integrationName=%s", integrationName) } @@ -346,8 +347,9 @@ func TestIntegrationEnabled(t *testing.T) { if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") || strings.Contains(pkg.ImportPath, "/cmd") { continue } - p := strings.Replace(pkg.Dir, pkg.Root, "../..", 1) - if strings.Contains(p, "/contrib/net/http/client") || strings.Contains(p, "/contrib/os") { + sep := string(os.PathSeparator) + p := strings.Replace(pkg.Dir, pkg.Root, filepath.Join("..", ".."), 1) + if strings.Contains(p, filepath.Join(sep, "contrib", "net", "http", "client")) || strings.Contains(p, filepath.Join(sep, "contrib", "os")) { continue } body, err := exec.Command("grep", "-rl", "MarkIntegrationImported", p).Output() diff --git a/ddtrace/tracer/slog.go b/ddtrace/tracer/slog.go index a4c9b24bd0..4c67445b73 100644 --- a/ddtrace/tracer/slog.go +++ b/ddtrace/tracer/slog.go @@ -13,11 +13,16 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) +// groupOrAttrs holds either a group name or a list of slog.Attrs. +type groupOrAttrs struct { + group string // group name if non-empty + attrs []slog.Attr // attrs if non-empty +} + // slogHandler implements the slog.Handler interface to dispatch messages to our // internal logger. type slogHandler struct { - attrs []string - groups []string + goas []groupOrAttrs } func (h slogHandler) Enabled(ctx context.Context, lvl slog.Level) bool { @@ -30,10 +35,30 @@ func (h slogHandler) Enabled(ctx context.Context, lvl slog.Level) bool { } func (h slogHandler) Handle(ctx context.Context, r slog.Record) error { - parts := make([]string, 0, len(h.attrs)+r.NumAttrs()) - parts = append(parts, h.attrs...) + goas := h.goas + + if r.NumAttrs() == 0 { + // If the record has no Attrs, remove groups at the end of the list; they are empty. + for len(goas) > 0 && goas[len(goas)-1].group != "" { + goas = goas[:len(goas)-1] + } + } + + parts := make([]string, 0, len(goas)+r.NumAttrs()) + formatGroup := "" + + for _, goa := range goas { + if goa.group != "" { + formatGroup += goa.group + "." + } else { + for _, a := range goa.attrs { + parts = append(parts, formatGroup+a.String()) + } + } + } + r.Attrs(func(a slog.Attr) bool { - parts = append(parts, formatAttr(a, h.groups)) + parts = append(parts, formatGroup+a.String()) return true }) @@ -51,18 +76,25 @@ func (h slogHandler) Handle(ctx context.Context, r slog.Record) error { return nil } -func (h slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - for _, a := range attrs { - h.attrs = append(h.attrs, formatAttr(a, h.groups)) - } +func (h slogHandler) withGroupOrAttrs(goa groupOrAttrs) slogHandler { + h.goas = append(h.goas, goa) return h } +// WithGroup returns a new Handler whose group consist of +// both the receiver's groups and the arguments. func (h slogHandler) WithGroup(name string) slog.Handler { - h.groups = append(h.groups, name) - return h + if name == "" { + return h + } + return h.withGroupOrAttrs(groupOrAttrs{group: name}) } -func formatAttr(a slog.Attr, groups []string) string { - return strings.Join(append(groups, a.String()), ".") +// WithAttrs returns a new Handler whose attributes consist of +// both the receiver's attributes and the arguments. +func (h slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) } diff --git a/ddtrace/tracer/slog_test.go b/ddtrace/tracer/slog_test.go index 352cac0183..047d350458 100644 --- a/ddtrace/tracer/slog_test.go +++ b/ddtrace/tracer/slog_test.go @@ -37,9 +37,17 @@ func Test_slogHandler(t *testing.T) { l.Error("error test", "n", 3) log.Flush() // needed to get the error log flushed + // Check that chaining works as expected. + l = l.With("baz", "qux") + l = l.WithGroup("c").WithGroup("d") + l.Info("info test", "n", 1) + + log.Flush() + // Check that the logs were written correctly. - require.Len(t, rl.Logs(), 3) + require.Len(t, rl.Logs(), 4) require.Contains(t, rl.Logs()[0], "info test foo=bar a.b.n=1") require.Contains(t, rl.Logs()[1], "warn test foo=bar a.b.n=2") require.Contains(t, rl.Logs()[2], "error test foo=bar a.b.n=3") + require.Contains(t, rl.Logs()[3], "info test foo=bar a.b.baz=qux a.b.c.d.n=1") } diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 20047c293e..816e641c55 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -10,6 +10,7 @@ package tracer import ( "context" "encoding/base64" + "encoding/json" "fmt" "os" "reflect" @@ -92,6 +93,13 @@ type span struct { taskEnd func() // ends execution tracer (runtime/trace) task, if started } +type SpanWithLinks interface { + ddtrace.Span + + // AddSpanLink appends the given link to span's span links. + AddSpanLink(link ddtrace.SpanLink) +} + // Context yields the SpanContext for this Span. Note that the return // value of Context() is still valid after a call to Finish(). This is // called the span context and it is different from Go's context. @@ -470,6 +478,27 @@ func (s *span) setMetric(key string, v float64) { } } +// AddSpanLink appends the given link to the span's span links. +func (s *span) AddSpanLink(link ddtrace.SpanLink) { + s.SpanLinks = append(s.SpanLinks, link) +} + +// serializeSpanLinksInMeta saves span links as a JSON string under `Span[meta][_dd.span_links]`. +func (s *span) serializeSpanLinksInMeta() { + if len(s.SpanLinks) == 0 { + return + } + spanLinkBytes, err := json.Marshal(s.SpanLinks) + if err != nil { + log.Debug("Unable to marshal span links. Not adding span links to span meta.") + return + } + if s.Meta == nil { + s.Meta = make(map[string]string) + } + s.Meta["_dd.span_links"] = string(spanLinkBytes) +} + // Finish closes this Span (but not its children) providing the duration // of its part of the tracing session. func (s *span) Finish(opts ...ddtrace.FinishOption) { @@ -520,6 +549,8 @@ func (s *span) Finish(opts ...ddtrace.FinishOption) { } } + s.serializeSpanLinksInMeta() + s.finish(t) orchestrion.GLSPopValue(sharedinternal.ActiveSpanKey) } diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 4c9b4d1c59..b37a637b8e 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -6,8 +6,10 @@ package tracer import ( + "encoding/json" "errors" "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "runtime" "strings" "sync" @@ -1051,6 +1053,27 @@ func BenchmarkSetTagField(b *testing.B) { } } +func BenchmarkSerializeSpanLinksInMeta(b *testing.B) { + span := newBasicSpan("bench.span") + + span.AddSpanLink(ddtrace.SpanLink{SpanID: 123, TraceID: 456}) + span.AddSpanLink(ddtrace.SpanLink{SpanID: 789, TraceID: 101}) + + // Sample span pointer + attributes := map[string]string{ + "link.kind": "span-pointer", + "ptr.dir": "d", + "ptr.hash": "eb29cb7d923f904f02bd8b3d85e228ed", + "ptr.kind": "aws.s3.object", + } + span.AddSpanLink(ddtrace.SpanLink{TraceID: 0, SpanID: 0, Attributes: attributes}) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + span.serializeSpanLinksInMeta() + } +} + type boomError struct{} func (e *boomError) Error() string { return "boom" } @@ -1092,3 +1115,43 @@ func testConcurrentSpanSetTag(t *testing.T) { } wg.Wait() } + +func TestSpanLinksInMeta(t *testing.T) { + t.Run("no_links", func(t *testing.T) { + tracer := newTracer() + defer tracer.Stop() + + sp := tracer.StartSpan("test-no-links") + sp.Finish() + + internalSpan := sp.(*span) + _, ok := internalSpan.Meta["_dd.span_links"] + assert.False(t, ok, "Expected no _dd.span_links in Meta.") + }) + + t.Run("with_links", func(t *testing.T) { + tracer := newTracer() + defer tracer.Stop() + + sp, ok := tracer.StartSpan("test-with-links").(SpanWithLinks) + require.True(t, ok, "Span does not implement SpanWithLinks interface") + + sp.AddSpanLink(ddtrace.SpanLink{SpanID: 123, TraceID: 456}) + sp.AddSpanLink(ddtrace.SpanLink{SpanID: 789, TraceID: 012}) + sp.Finish() + + internalSpan := sp.(*span) + raw, ok := internalSpan.Meta["_dd.span_links"] + require.True(t, ok, "Expected _dd.span_links in Meta after adding links.") + + var links []ddtrace.SpanLink + err := json.Unmarshal([]byte(raw), &links) + require.NoError(t, err, "Failed to unmarshal links JSON") + require.Len(t, links, 2, "Expected 2 links in _dd.span_links JSON") + + assert.Equal(t, uint64(123), links[0].SpanID) + assert.Equal(t, uint64(456), links[0].TraceID) + assert.Equal(t, uint64(789), links[1].SpanID) + assert.Equal(t, uint64(012), links[1].TraceID) + }) +} diff --git a/ddtrace/tracer/stats.go b/ddtrace/tracer/stats.go index bf8530a8d6..687059dcc3 100644 --- a/ddtrace/tracer/stats.go +++ b/ddtrace/tracer/stats.go @@ -70,12 +70,17 @@ func newConcentrator(c *config, bucketSize int64, statsdClient internal.StatsdCl env = "unknown-env" log.Debug("No DD Env found, normally the agent should have one") } + gitCommitSha := "" + if c.ciVisibilityEnabled { + // We only have this data if we're in CI Visibility + gitCommitSha = utils.GetCITags()[constants.GitCommitSHA] + } aggKey := stats.PayloadAggregationKey{ Hostname: c.hostname, Env: env, Version: c.version, ContainerID: "", // This intentionally left empty as the Agent will attach the container ID only in certain situations. - GitCommitSha: utils.GetCITags()[constants.GitCommitSHA], + GitCommitSha: gitCommitSha, ImageTag: "", } spanConcentrator := stats.NewSpanConcentrator(sCfg, time.Now()) diff --git a/ddtrace/tracer/stats_test.go b/ddtrace/tracer/stats_test.go index 42f0bc17b0..503552adcf 100644 --- a/ddtrace/tracer/stats_test.go +++ b/ddtrace/tracer/stats_test.go @@ -105,7 +105,7 @@ func TestConcentrator(t *testing.T) { t.Run("ciGitSha", func(t *testing.T) { utils.AddCITags(constants.GitCommitSHA, "DEADBEEF") transport := newDummyTransport() - c := newConcentrator(&config{transport: transport, env: "someEnv"}, (10 * time.Second).Nanoseconds(), &statsd.NoOpClientDirect{}) + c := newConcentrator(&config{transport: transport, env: "someEnv", ciVisibilityEnabled: true}, (10 * time.Second).Nanoseconds(), &statsd.NoOpClientDirect{}) assert.Len(t, transport.Stats(), 0) ss1, ok := c.newTracerStatSpan(&s1, nil) assert.True(t, ok) diff --git a/ddtrace/tracer/textmap.go b/ddtrace/tracer/textmap.go index f7367adb83..b1698ef48d 100644 --- a/ddtrace/tracer/textmap.go +++ b/ddtrace/tracer/textmap.go @@ -273,15 +273,22 @@ func (p *chainedPropagator) Inject(spanCtx ddtrace.SpanContext, carrier interfac func (p *chainedPropagator) Extract(carrier interface{}) (ddtrace.SpanContext, error) { var ctx ddtrace.SpanContext var links []ddtrace.SpanLink + for _, v := range p.extractors { firstExtract := (ctx == nil) // ctx stores the most recently extracted ctx across iterations; if it's nil, no extractor has run yet extractedCtx, err := v.Extract(carrier) + if firstExtract { - if err != nil && err != ErrSpanContextNotFound { // We only care if the first extraction returns an error because this breaks distributed tracing - return nil, err + if err != nil { + if p.onlyExtractFirst { // Every error is relevant when we are relying on the first extractor + return nil, err + } + if err != ErrSpanContextNotFound { // We don't care about ErrSpanContextNotFound because we could find a span context in a subsequent extractor + return nil, err + } } - if p.onlyExtractFirst { // Return early if only performing one extraction - return extractedCtx.(*spanContext), nil + if p.onlyExtractFirst { + return extractedCtx, nil } ctx = extractedCtx } else { // A local trace context has already been extracted diff --git a/ddtrace/tracer/textmap_test.go b/ddtrace/tracer/textmap_test.go index 74038fc0c7..0dd05124c0 100644 --- a/ddtrace/tracer/textmap_test.go +++ b/ddtrace/tracer/textmap_test.go @@ -2244,6 +2244,49 @@ func TestOtelPropagator(t *testing.T) { } } +// Assert that extraction returns a ErrSpanContextNotFound error when no trace context headers are found +func TestExtractNoHeaders(t *testing.T) { + tests := []struct { + name string + extractEnv string + extractFirst bool + }{ + { + name: "single header", + extractEnv: "datadog", + extractFirst: false, + }, + { + name: "single header - extractFirst", + extractEnv: "datadog", + extractFirst: true, + }, + { + name: "multi header", + extractEnv: "datadog,tracecontext", + extractFirst: false, + }, + { + name: "multi header - extractFirst", + extractEnv: "datadog,tracecontext", + extractFirst: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(headerPropagationStyleExtract, tt.extractEnv) + if tt.extractFirst { + t.Setenv("DD_TRACE_PROPAGATION_EXTRACT_FIRST", "true") + } + tracer := newTracer() + defer tracer.Stop() + ctx, err := tracer.Extract(TextMapCarrier{}) + assert.Equal(t, ErrSpanContextNotFound, err) + assert.Nil(t, ctx) + }) + } +} + func BenchmarkInjectDatadog(b *testing.B) { b.Setenv(headerPropagationStyleInject, "datadog") tracer := newTracer() diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 352dd32ee0..8413cd1c8b 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -174,8 +174,6 @@ func Start(opts ...StartOption) { // DD_INSTRUMENTATION_TELEMETRY_ENABLED env var startTelemetry(t.config) - // start appsec - appsec.Start(t.config.appsecStartOptions...) _ = t.hostname() // Prime the hostname cache return } diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index 222f6c8b70..1cf84254ad 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -104,6 +104,9 @@ func newHTTPTransport(url string, client *http.Client) *httpTransport { if eid := internal.EntityID(); eid != "" { defaultHeaders["Datadog-Entity-ID"] = eid } + if extEnv := internal.ExternalEnvironment(); extEnv != "" { + defaultHeaders["Datadog-External-Env"] = extEnv + } return &httpTransport{ traceURL: fmt.Sprintf("%s/v0.4/traces", url), statsURL: fmt.Sprintf("%s/v0.6/stats", url), diff --git a/ddtrace/tracer/transport_test.go b/ddtrace/tracer/transport_test.go index b8967713cf..efcafde18f 100644 --- a/ddtrace/tracer/transport_test.go +++ b/ddtrace/tracer/transport_test.go @@ -396,3 +396,30 @@ func TestWithUDS(t *testing.T) { assert.Len(rt.reqs, 1) assert.Equal(hits, 2) } + +func TestExternalEnvironment(t *testing.T) { + t.Setenv("DD_EXTERNAL_ENV", "it-false,cn-nginx-webserver,pu-75a2b6d5-3949-4afb-ad0d-92ff0674e759") + assert := assert.New(t) + found := false + srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + extEnv := r.Header.Get("Datadog-External-Env") + if extEnv == "" { + return + } + assert.Equal("it-false,cn-nginx-webserver,pu-75a2b6d5-3949-4afb-ad0d-92ff0674e759", extEnv) + found = true + })) + defer srv.Close() + + u, err := url.Parse(srv.URL) + assert.NoError(err) + c := &http.Client{} + trc := newTracer(WithAgentTimeout(2), WithAgentAddr(u.Host), WithHTTPClient(c)) + defer trc.Stop() + + p, err := encode(getTestTrace(1, 1)) + assert.NoError(err) + _, err = trc.config.transport.send(p) + assert.NoError(err) + assert.True(found) +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 76680be932..8eae54d8a3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -41,6 +41,12 @@ services: image: redis:3.2 ports: - "6379:6379" + valkey: + image: valkey/valkey:8 + environment: + VALKEY_EXTRA_FLAGS: "--port 6380 --requirepass password-for-default" + ports: + - "6380:6380" elasticsearch2: image: elasticsearch:2 environment: diff --git a/go.mod b/go.mod index 11b922c3d9..ee503ab08a 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module gopkg.in/DataDog/dd-trace-go.v1 go 1.22.0 +// This replace is a temporary workaround to deal with a breaking change here that is used by the datadog-agent +// It can safely be removed once this PR is released: https://github.com/DataDog/datadog-agent/pull/33370 +replace github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes => github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 + require ( cloud.google.com/go/pubsub v1.37.0 github.com/99designs/gqlgen v0.17.36 @@ -90,6 +94,7 @@ require ( github.com/uptrace/bun v1.1.17 github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 github.com/urfave/negroni v1.0.0 + github.com/valkey-io/valkey-go v1.0.52 github.com/valyala/fasthttp v1.51.0 github.com/vektah/gqlparser/v2 v2.5.16 github.com/zenazn/goji v1.0.1 @@ -295,7 +300,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 8019a928b9..fc141ad446 100644 --- a/go.sum +++ b/go.sum @@ -798,8 +798,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= -github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -990,6 +990,8 @@ github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 h1:i8NFU9r8YuavNFaYlNqi4ppn github.com/uptrace/bun/dialect/sqlitedialect v1.1.17/go.mod h1:YF0FO4VVnY9GHNH6rM4r3STlVEBxkOc6L88Bm5X5mzA= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valkey-io/valkey-go v1.0.52 h1:ojrR736satGucqpllYzal8fUrNNROc11V10zokAyIYg= +github.com/valkey-io/valkey-go v1.0.52/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -1120,8 +1122,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/internal/civisibility/constants/test_tags.go b/internal/civisibility/constants/test_tags.go index d3b30d461b..98c4bd9052 100644 --- a/internal/civisibility/constants/test_tags.go +++ b/internal/civisibility/constants/test_tags.go @@ -78,6 +78,9 @@ const ( // This constant is used to tag test events that are part of a retry execution TestIsRetry = "test.is_retry" + // TestRetryReason indicates the reason for retrying the test + TestRetryReason = "test.retry_reason" + // TestEarlyFlakeDetectionRetryAborted indicates a retry abort reason by the early flake detection feature TestEarlyFlakeDetectionRetryAborted = "test.early_flake.abort_reason" diff --git a/internal/civisibility/integrations/civisibility_features.go b/internal/civisibility/integrations/civisibility_features.go index 6b82bccf72..1155624433 100644 --- a/internal/civisibility/integrations/civisibility_features.go +++ b/internal/civisibility/integrations/civisibility_features.go @@ -51,8 +51,8 @@ var ( // ciVisibilitySettings contains the CI Visibility settings for this session ciVisibilitySettings net.SettingsResponseData - // ciVisibilityEarlyFlakyDetectionSettings contains the CI Visibility Early Flake Detection data for this session - ciVisibilityEarlyFlakyDetectionSettings net.EfdResponseData + // ciVisibilityKnownTests contains the CI Visibility Known Tests data for this session + ciVisibilityKnownTests net.KnownTestsResponseData // ciVisibilityFlakyRetriesSettings contains the CI Visibility Flaky Retries settings for this session ciVisibilityFlakyRetriesSettings FlakyRetriesSetting @@ -121,15 +121,20 @@ func ensureAdditionalFeaturesInitialization(serviceName string) { return } - // if early flake detection is enabled then we run the early flake detection request - if ciVisibilitySettings.EarlyFlakeDetection.Enabled { - ciEfdData, err := ciVisibilityClient.GetEarlyFlakeDetectionData() + // if early flake detection is enabled then we run the known tests request + if ciVisibilitySettings.KnownTestsEnabled { + ciEfdData, err := ciVisibilityClient.GetKnownTests() if err != nil { - log.Error("civisibility: error getting CI visibility early flake detection data: %v", err) + log.Error("civisibility: error getting CI visibility known tests data: %v", err) } else if ciEfdData != nil { - ciVisibilityEarlyFlakyDetectionSettings = *ciEfdData - log.Debug("civisibility: early flake detection data loaded.") + ciVisibilityKnownTests = *ciEfdData + log.Debug("civisibility: known tests data loaded.") } + } else { + // "known_tests_enabled" parameter works as a kill-switch for EFD, so if “known_tests_enabled” is false it + // will disable EFD even if “early_flake_detection.enabled” is set to true (which should not happen normally, + // the backend should disable both of them in that case) + ciVisibilitySettings.EarlyFlakeDetection.Enabled = false } // if flaky test retries is enabled then let's load the flaky retries settings @@ -172,11 +177,11 @@ func GetSettings() *net.SettingsResponseData { return &ciVisibilitySettings } -// GetEarlyFlakeDetectionSettings gets the early flake detection known tests data -func GetEarlyFlakeDetectionSettings() *net.EfdResponseData { +// GetKnownTests gets the known tests data +func GetKnownTests() *net.KnownTestsResponseData { // call to ensure the additional features initialization is completed (service name can be null here) ensureAdditionalFeaturesInitialization("") - return &ciVisibilityEarlyFlakyDetectionSettings + return &ciVisibilityKnownTests } // GetFlakyRetriesSettings gets the flaky retries settings diff --git a/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go b/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go index e3c59cea8f..657da02f88 100644 --- a/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go +++ b/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go @@ -73,7 +73,7 @@ type MockClient struct { SendCoveragePayloadFunc func(ciTestCovPayload io.Reader) error SendCoveragePayloadWithFormatFunc func(ciTestCovPayload io.Reader, format string) error GetSettingsFunc func() (*net.SettingsResponseData, error) - GetEarlyFlakeDetectionDataFunc func() (*net.EfdResponseData, error) + GetKnownTestsFunc func() (*net.KnownTestsResponseData, error) GetCommitsFunc func(localCommits []string) ([]string, error) SendPackFilesFunc func(commitSha string, packFiles []string) (bytes int64, err error) GetSkippableTestsFunc func() (correlationId string, skippables map[string]map[string][]net.SkippableResponseDataAttributes, err error) @@ -91,8 +91,8 @@ func (m *MockClient) GetSettings() (*net.SettingsResponseData, error) { return m.GetSettingsFunc() } -func (m *MockClient) GetEarlyFlakeDetectionData() (*net.EfdResponseData, error) { - return m.GetEarlyFlakeDetectionDataFunc() +func (m *MockClient) GetKnownTests() (*net.KnownTestsResponseData, error) { + return m.GetKnownTestsFunc() } func (m *MockClient) GetCommits(localCommits []string) ([]string, error) { diff --git a/internal/civisibility/integrations/gotesting/instrumentation.go b/internal/civisibility/integrations/gotesting/instrumentation.go index 1c3be06223..45154a3417 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation.go +++ b/internal/civisibility/integrations/gotesting/instrumentation.go @@ -9,7 +9,6 @@ import ( "fmt" "reflect" "runtime" - "slices" "sync" "sync/atomic" "testing" @@ -36,7 +35,9 @@ type ( panicData any // panic data recovered from an internal test execution when using an additional feature wrapper panicStacktrace string // stacktrace from the panic recovered from an internal test isARetry bool // flag to tag if a current test execution is a retry - isANewTest bool // flag to tag if a current test execution is part of a new test (EFD not known test) + isANewTest bool // flag to tag if a current test execution is part of a new test + isEFDExecution bool // flag to tag if a current test execution is part of an EFD execution + isATRExecution bool // flag to tag if a current test execution is part of an ATR execution hasAdditionalFeatureWrapper bool // flag to check if the current execution is part of an additional feature wrapper } @@ -234,7 +235,10 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(* } } }, - execMetaAdjust: nil, // No execMetaAdjust needed + execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { + // Set the flag ATR execution to true + execMeta.isATRExecution = true + }, }) }, true } @@ -243,95 +247,82 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(* // applyEarlyFlakeDetectionAdditionalFeature applies the early flake detection feature as a wrapper of a func(*testing.T) func applyEarlyFlakeDetectionAdditionalFeature(testInfo *commonInfo, targetFunc func(*testing.T), settings *net.SettingsResponseData) (func(*testing.T), bool) { - earlyFlakeDetectionData := integrations.GetEarlyFlakeDetectionSettings() - if earlyFlakeDetectionData != nil && - len(earlyFlakeDetectionData.Tests) > 0 { - - // Define is a known test flag - isAKnownTest := false - - // Check if the test is a known test or a new one - if knownSuites, ok := earlyFlakeDetectionData.Tests[testInfo.moduleName]; ok { - if knownTests, ok := knownSuites[testInfo.suiteName]; ok { - if slices.Contains(knownTests, testInfo.testName) { - isAKnownTest = true - } - } - } + isKnown, hasKnownData := isKnownTest(testInfo) + if !hasKnownData || isKnown { + return targetFunc, false + } - // If it's a new test, then we apply the EFD wrapper - if !isAKnownTest { - return func(t *testing.T) { - var testPassCount, testSkipCount, testFailCount int - - runTestWithRetry(&runTestWithRetryOptions{ - targetFunc: targetFunc, - t: t, - initialRetryCount: 0, - adjustRetryCount: func(duration time.Duration) int64 { - slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries - durationSecs := duration.Seconds() - if durationSecs < 5 { - return int64(slowTestRetriesSettings.FiveS) - } else if durationSecs < 10 { - return int64(slowTestRetriesSettings.TenS) - } else if durationSecs < 30 { - return int64(slowTestRetriesSettings.ThirtyS) - } else if duration.Minutes() < 5 { - return int64(slowTestRetriesSettings.FiveM) - } - return 0 - }, - shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool { - return remainingRetries >= 0 - }, - perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) { - // Collect test results - if ptrToLocalT.Failed() { - testFailCount++ - } else if ptrToLocalT.Skipped() { - testSkipCount++ - } else { - testPassCount++ - } - }, - onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) { - // Update test status based on collected counts - tCommonPrivates := getTestPrivateFields(t) - if tCommonPrivates == nil { - panic("getting test private fields failed") - } - status := "passed" - if testPassCount == 0 { - if testSkipCount > 0 { - status = "skipped" - tCommonPrivates.SetSkipped(true) - } - if testFailCount > 0 { - status = "failed" - tCommonPrivates.SetFailed(true) - tParentCommonPrivates := getTestParentPrivateFields(t) - if tParentCommonPrivates == nil { - panic("getting test parent private fields failed") - } - tParentCommonPrivates.SetFailed(true) - } + // If it's a new test, then we apply the EFD wrapper + return func(t *testing.T) { + var testPassCount, testSkipCount, testFailCount int + + runTestWithRetry(&runTestWithRetryOptions{ + targetFunc: targetFunc, + t: t, + initialRetryCount: 0, + adjustRetryCount: func(duration time.Duration) int64 { + slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries + durationSecs := duration.Seconds() + if durationSecs < 5 { + return int64(slowTestRetriesSettings.FiveS) + } else if durationSecs < 10 { + return int64(slowTestRetriesSettings.TenS) + } else if durationSecs < 30 { + return int64(slowTestRetriesSettings.ThirtyS) + } else if duration.Minutes() < 5 { + return int64(slowTestRetriesSettings.FiveM) + } + return 0 + }, + shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool { + return remainingRetries >= 0 + }, + perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) { + // Collect test results + if ptrToLocalT.Failed() { + testFailCount++ + } else if ptrToLocalT.Skipped() { + testSkipCount++ + } else { + testPassCount++ + } + }, + onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) { + // Update test status based on collected counts + tCommonPrivates := getTestPrivateFields(t) + if tCommonPrivates == nil { + panic("getting test private fields failed") + } + status := "passed" + if testPassCount == 0 { + if testSkipCount > 0 { + status = "skipped" + tCommonPrivates.SetSkipped(true) + } + if testFailCount > 0 { + status = "failed" + tCommonPrivates.SetFailed(true) + tParentCommonPrivates := getTestParentPrivateFields(t) + if tParentCommonPrivates == nil { + panic("getting test parent private fields failed") } + tParentCommonPrivates.SetFailed(true) + } + } - // Print summary after retries - if executionIndex > 0 { - fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex) - } - }, - execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { - // Set the flag new test to true - execMeta.isANewTest = true - }, - }) - }, true - } - } - return targetFunc, false + // Print summary after retries + if executionIndex > 0 { + fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex) + } + }, + execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { + // Set the flag new test to true + execMeta.isANewTest = true + // Set the flag EFD execution to true + execMeta.isEFDExecution = true + }, + }) + }, true } // runTestWithRetry encapsulates the common retry logic for test functions. @@ -386,6 +377,12 @@ func runTestWithRetry(options *runTestWithRetryOptions) { if originalExecMeta.isARetry { execMeta.isARetry = true } + if originalExecMeta.isEFDExecution { + execMeta.isEFDExecution = true + } + if originalExecMeta.isATRExecution { + execMeta.isATRExecution = true + } } // If we are in a retry execution, set the `isARetry` flag diff --git a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go index b87a4b9c13..c86b361214 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go +++ b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go @@ -170,6 +170,12 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { if parentExecMeta.isARetry { execMeta.isARetry = true } + if parentExecMeta.isEFDExecution { + execMeta.isEFDExecution = true + } + if parentExecMeta.isATRExecution { + execMeta.isATRExecution = true + } } } @@ -186,6 +192,15 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { if execMeta.isARetry { // Set the retry tag test.SetTag(constants.TestIsRetry, "true") + + // If the execution is an EFD execution we tag the test event reason + if execMeta.isEFDExecution { + // Set the EFD as the retry reason + test.SetTag(constants.TestRetryReason, "efd") + } else if execMeta.isATRExecution { + // Set the ATR as the retry reason + test.SetTag(constants.TestRetryReason, "atr") + } } defer func() { diff --git a/internal/civisibility/integrations/gotesting/testcontroller_test.go b/internal/civisibility/integrations/gotesting/testcontroller_test.go index 1ab6347f84..7a5dc25fbb 100644 --- a/internal/civisibility/integrations/gotesting/testcontroller_test.go +++ b/internal/civisibility/integrations/gotesting/testcontroller_test.go @@ -74,7 +74,19 @@ func TestMain(m *testing.M) { func runFlakyTestRetriesTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, false, nil, false, nil) + server := setUpHttpServer(true, true, false, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ + "reflections_test.go": []string{ + "TestGetFieldPointerFrom", + "TestGetInternalTestArray", + "TestGetInternalBenchmarkArray", + "TestCommonPrivateFields_AddLevel", + "TestGetBenchmarkPrivateFields", + }, + }, + }, + }, false, nil) defer server.Close() // set a custom retry count @@ -137,6 +149,13 @@ func runFlakyTestRetriesTests(m *testing.M) { // check spans by tag checkSpansByTagName(finishedSpans, constants.TestIsRetry, 6) + trrSpan := checkSpansByTagName(finishedSpans, constants.TestRetryReason, 6)[0] + if trrSpan.Tag(constants.TestRetryReason) != "atr" { + panic(fmt.Sprintf("expected retry reason to be %s, got %s", "atr", trrSpan.Tag(constants.TestRetryReason))) + } + + // check the test is new tag + checkSpansByTagName(finishedSpans, constants.TestIsNew, 22) // check spans by type checkSpansByType(finishedSpans, @@ -153,9 +172,9 @@ func runFlakyTestRetriesTests(m *testing.M) { func runEarlyFlakyTestDetectionTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(false, true, &net.EfdResponseData{ - Tests: net.EfdResponseDataModules{ - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.EfdResponseDataSuites{ + server := setUpHttpServer(false, true, true, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ "reflections_test.go": []string{ "TestGetFieldPointerFrom", "TestGetInternalTestArray", @@ -227,6 +246,10 @@ func runEarlyFlakyTestDetectionTests(m *testing.M) { // check spans by tag checkSpansByTagName(finishedSpans, constants.TestIsNew, 176) checkSpansByTagName(finishedSpans, constants.TestIsRetry, 160) + trrSpan := checkSpansByTagName(finishedSpans, constants.TestRetryReason, 160)[0] + if trrSpan.Tag(constants.TestRetryReason) != "efd" { + panic(fmt.Sprintf("expected retry reason to be %s, got %s", "efd", trrSpan.Tag(constants.TestRetryReason))) + } // check spans by type checkSpansByType(finishedSpans, @@ -243,9 +266,9 @@ func runEarlyFlakyTestDetectionTests(m *testing.M) { func runFlakyTestRetriesWithEarlyFlakyTestDetectionTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, true, &net.EfdResponseData{ - Tests: net.EfdResponseDataModules{ - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.EfdResponseDataSuites{ + server := setUpHttpServer(true, true, true, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ "reflections_test.go": []string{ "TestGetFieldPointerFrom", "TestGetInternalTestArray", @@ -355,7 +378,7 @@ func runFlakyTestRetriesWithEarlyFlakyTestDetectionTests(m *testing.M) { func runIntelligentTestRunnerTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, false, nil, true, []net.SkippableResponseDataAttributes{ + server := setUpHttpServer(true, true, false, nil, true, []net.SkippableResponseDataAttributes{ { Suite: "testing_test.go", Name: "TestMyTest01", @@ -569,8 +592,10 @@ type ( ) func setUpHttpServer(flakyRetriesEnabled bool, - earlyFlakyDetectionEnabled bool, earlyFlakyDetectionData *net.EfdResponseData, + knownTestsEnabled bool, + earlyFlakyDetectionEnabled bool, earlyFlakyDetectionData *net.KnownTestsResponseData, itrEnabled bool, itrData []net.SkippableResponseDataAttributes) *httptest.Server { + enableKnownTests := knownTestsEnabled || earlyFlakyDetectionEnabled // mock the settings api to enable automatic test retries server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Printf("MockApi received request: %s\n", r.URL.Path) @@ -591,6 +616,7 @@ func setUpHttpServer(flakyRetriesEnabled bool, FlakyTestRetriesEnabled: flakyRetriesEnabled, ItrEnabled: itrEnabled, TestsSkipping: itrEnabled, + KnownTestsEnabled: enableKnownTests, } response.Data.Attributes.EarlyFlakeDetection.Enabled = earlyFlakyDetectionEnabled response.Data.Attributes.EarlyFlakeDetection.SlowTestRetries.FiveS = 10 @@ -600,13 +626,13 @@ func setUpHttpServer(flakyRetriesEnabled bool, fmt.Printf("MockApi sending response: %v\n", response) json.NewEncoder(w).Encode(&response) - } else if earlyFlakyDetectionEnabled && r.URL.Path == "/api/v2/ci/libraries/tests" { + } else if enableKnownTests && r.URL.Path == "/api/v2/ci/libraries/tests" { w.Header().Set("Content-Type", "application/json") response := struct { Data struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes net.EfdResponseData `json:"attributes"` + ID string `json:"id"` + Type string `json:"type"` + Attributes net.KnownTestsResponseData `json:"attributes"` } `json:"data,omitempty"` }{} diff --git a/internal/civisibility/integrations/gotesting/testing.go b/internal/civisibility/integrations/gotesting/testing.go index a23d6665d3..4ef0865fb0 100644 --- a/internal/civisibility/integrations/gotesting/testing.go +++ b/internal/civisibility/integrations/gotesting/testing.go @@ -9,6 +9,7 @@ import ( "fmt" "reflect" "runtime" + "slices" "sync/atomic" "testing" "time" @@ -168,6 +169,7 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { settings := integrations.GetSettings() coverageEnabled := settings.CodeCoverage testSkippedByITR := false + testIsNew := true // Check if the test is going to be skipped by ITR if settings.ItrEnabled && settings.TestsSkipping { @@ -182,6 +184,15 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { } } + // Check if the test is known + if settings.KnownTestsEnabled { + testIsKnown, testKnownDataOk := isKnownTest(&testInfo.commonInfo) + testIsNew = testKnownDataOk && !testIsKnown + } else { + // We don't mark any test as new if the feature is disabled + testIsNew = false + } + // Instrument the test function instrumentedFunc := func(t *testing.T) { // Set this func as a helper func of t @@ -204,7 +215,8 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { // Set the CI Visibility test to the execution metadata execMeta.test = test - // If the execution is for a new test we tag the test event from early flake detection + // If the execution is for a new test we tag the test event as new + execMeta.isANewTest = execMeta.isANewTest || testIsNew if execMeta.isANewTest { // Set the is new test tag test.SetTag(constants.TestIsNew, "true") @@ -214,6 +226,15 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { if execMeta.isARetry { // Set the retry tag test.SetTag(constants.TestIsRetry, "true") + + // If the execution is an EFD execution we tag the test event reason + if execMeta.isEFDExecution { + // Set the EFD as the retry reason + test.SetTag(constants.TestRetryReason, "efd") + } else if execMeta.isATRExecution { + // Set the ATR as the retry reason + test.SetTag(constants.TestRetryReason, "atr") + } } // Check if the test needs to be skipped by ITR @@ -385,6 +406,19 @@ func (ddm *M) instrumentInternalBenchmarks(internalBenchmarks *[]testing.Interna // executeInternalBenchmark wraps the original benchmark function to include CI visibility instrumentation. func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testing.B) { originalFunc := runtime.FuncForPC(reflect.Indirect(reflect.ValueOf(benchmarkInfo.originalFunc)).Pointer()) + + settings := integrations.GetSettings() + testIsNew := true + + // Check if the test is known + if settings.KnownTestsEnabled { + testIsKnown, testKnownDataOk := isKnownTest(&benchmarkInfo.commonInfo) + testIsNew = testKnownDataOk && !testIsKnown + } else { + // We don't mark any test as new if the feature is disabled + testIsNew = false + } + instrumentedInternalFunc := func(b *testing.B) { // decrement level @@ -399,6 +433,12 @@ func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testin test := suite.CreateTest(benchmarkInfo.testName, integrations.WithTestStartTime(startTime)) test.SetTestFunc(originalFunc) + // If the execution is for a new test we tag the test event as new + if testIsNew { + // Set the is new test tag + test.SetTag(constants.TestIsNew, "true") + } + // Run the original benchmark function. var iPfOfB *benchmarkPrivateFields var recoverFunc *func(r any) @@ -528,3 +568,20 @@ func checkModuleAndSuite(module integrations.TestModule, suite integrations.Test module.Close() } } + +// isKnownTest checks if a test is a known test or a new one +func isKnownTest(testInfo *commonInfo) (isKnown bool, hasKnownData bool) { + knownTestsData := integrations.GetKnownTests() + if knownTestsData != nil && len(knownTestsData.Tests) > 0 { + // Check if the test is a known test or a new one + if knownSuites, ok := knownTestsData.Tests[testInfo.moduleName]; ok { + if knownTests, ok := knownSuites[testInfo.suiteName]; ok { + return slices.Contains(knownTests, testInfo.testName), true + } + } + + return false, true + } + + return false, false +} diff --git a/internal/civisibility/utils/net/client.go b/internal/civisibility/utils/net/client.go index 8bca5e2440..3bcfcdd847 100644 --- a/internal/civisibility/utils/net/client.go +++ b/internal/civisibility/utils/net/client.go @@ -29,16 +29,16 @@ import ( const ( // DefaultMaxRetries is the default number of retries for a request. - DefaultMaxRetries int = 5 + DefaultMaxRetries int = 3 // DefaultBackoff is the default backoff time for a request. - DefaultBackoff time.Duration = 150 * time.Millisecond + DefaultBackoff time.Duration = 100 * time.Millisecond ) type ( // Client is an interface for sending requests to the Datadog backend. Client interface { GetSettings() (*SettingsResponseData, error) - GetEarlyFlakeDetectionData() (*EfdResponseData, error) + GetKnownTests() (*KnownTestsResponseData, error) GetCommits(localCommits []string) ([]string, error) SendPackFiles(commitSha string, packFiles []string) (bytes int64, err error) SendCoveragePayload(ciTestCovPayload io.Reader) error diff --git a/internal/civisibility/utils/net/efd_api.go b/internal/civisibility/utils/net/efd_api.go deleted file mode 100644 index 898e13603a..0000000000 --- a/internal/civisibility/utils/net/efd_api.go +++ /dev/null @@ -1,116 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2024 Datadog, Inc. - -package net - -import ( - "fmt" - "time" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" -) - -const ( - efdRequestType string = "ci_app_libraries_tests_request" - efdURLPath string = "api/v2/ci/libraries/tests" -) - -type ( - efdRequest struct { - Data efdRequestHeader `json:"data"` - } - - efdRequestHeader struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes EfdRequestData `json:"attributes"` - } - - EfdRequestData struct { - Service string `json:"service"` - Env string `json:"env"` - RepositoryURL string `json:"repository_url"` - Configurations testConfigurations `json:"configurations"` - } - - efdResponse struct { - Data struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes EfdResponseData `json:"attributes"` - } `json:"data"` - } - - EfdResponseData struct { - Tests EfdResponseDataModules `json:"tests"` - } - - EfdResponseDataModules map[string]EfdResponseDataSuites - EfdResponseDataSuites map[string][]string -) - -func (c *client) GetEarlyFlakeDetectionData() (*EfdResponseData, error) { - if c.repositoryURL == "" || c.commitSha == "" { - return nil, fmt.Errorf("civisibility.GetEarlyFlakeDetectionData: repository URL and commit SHA are required") - } - - body := efdRequest{ - Data: efdRequestHeader{ - ID: c.id, - Type: efdRequestType, - Attributes: EfdRequestData{ - Service: c.serviceName, - Env: c.environment, - RepositoryURL: c.repositoryURL, - Configurations: c.testConfigurations, - }, - }, - } - - request := c.getPostRequestConfig(efdURLPath, body) - if request.Compressed { - telemetry.EarlyFlakeDetectionRequest(telemetry.CompressedRequestCompressedType) - } else { - telemetry.EarlyFlakeDetectionRequest(telemetry.UncompressedRequestCompressedType) - } - - startTime := time.Now() - response, err := c.handler.SendRequest(*request) - telemetry.EarlyFlakeDetectionRequestMs(float64(time.Since(startTime).Milliseconds())) - - if err != nil { - telemetry.EarlyFlakeDetectionRequestErrors(telemetry.NetworkErrorType) - return nil, fmt.Errorf("sending early flake detection request: %s", err.Error()) - } - - if response.StatusCode < 200 || response.StatusCode >= 300 { - telemetry.EarlyFlakeDetectionRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) - } - if response.Compressed { - telemetry.EarlyFlakeDetectionResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) - } else { - telemetry.EarlyFlakeDetectionResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) - } - - var responseObject efdResponse - err = response.Unmarshal(&responseObject) - if err != nil { - return nil, fmt.Errorf("unmarshalling early flake detection data response: %s", err.Error()) - } - - testCount := 0 - if responseObject.Data.Attributes.Tests != nil { - for _, suites := range responseObject.Data.Attributes.Tests { - if suites == nil { - continue - } - for _, tests := range suites { - testCount += len(tests) - } - } - } - telemetry.EarlyFlakeDetectionResponseTests(float64(testCount)) - return &responseObject.Data.Attributes, nil -} diff --git a/internal/civisibility/utils/net/http.go b/internal/civisibility/utils/net/http.go index 71c402cd16..1d122a323a 100644 --- a/internal/civisibility/utils/net/http.go +++ b/internal/civisibility/utils/net/http.go @@ -358,7 +358,7 @@ func decompressData(data []byte) ([]byte, error) { // exponentialBackoff performs an exponential backoff with retries. func exponentialBackoff(retryCount int, initialDelay time.Duration) { - maxDelay := 30 * time.Second + maxDelay := 10 * time.Second delay := initialDelay * (1 << uint(retryCount)) // Exponential backoff if delay > maxDelay { delay = maxDelay diff --git a/internal/civisibility/utils/net/http_test.go b/internal/civisibility/utils/net/http_test.go index abf9ac4808..a9b8b155ed 100644 --- a/internal/civisibility/utils/net/http_test.go +++ b/internal/civisibility/utils/net/http_test.go @@ -786,9 +786,9 @@ func TestSendRequestWithInvalidRetryAfterHeader(t *testing.T) { func TestExponentialBackoffWithMaxDelay(t *testing.T) { start := time.Now() - exponentialBackoff(10, 1*time.Second) // Should be limited to maxDelay (30s) + exponentialBackoff(10, 1*time.Second) // Should be limited to maxDelay (10s) duration := time.Since(start) - assert.LessOrEqual(t, duration, 31*time.Second) + assert.LessOrEqual(t, duration, 11*time.Second) } func TestSendRequestWithContextTimeout(t *testing.T) { diff --git a/internal/civisibility/utils/net/known_tests_api.go b/internal/civisibility/utils/net/known_tests_api.go new file mode 100644 index 0000000000..ecaeb638a2 --- /dev/null +++ b/internal/civisibility/utils/net/known_tests_api.go @@ -0,0 +1,116 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package net + +import ( + "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" +) + +const ( + knownTestsRequestType string = "ci_app_libraries_tests_request" + knownTestsURLPath string = "api/v2/ci/libraries/tests" +) + +type ( + knownTestsRequest struct { + Data knownTestsRequestHeader `json:"data"` + } + + knownTestsRequestHeader struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes KnownTestsRequestData `json:"attributes"` + } + + KnownTestsRequestData struct { + Service string `json:"service"` + Env string `json:"env"` + RepositoryURL string `json:"repository_url"` + Configurations testConfigurations `json:"configurations"` + } + + knownTestsResponse struct { + Data struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes KnownTestsResponseData `json:"attributes"` + } `json:"data"` + } + + KnownTestsResponseData struct { + Tests KnownTestsResponseDataModules `json:"tests"` + } + + KnownTestsResponseDataModules map[string]KnownTestsResponseDataSuites + KnownTestsResponseDataSuites map[string][]string +) + +func (c *client) GetKnownTests() (*KnownTestsResponseData, error) { + if c.repositoryURL == "" || c.commitSha == "" { + return nil, fmt.Errorf("civisibility.GetKnownTests: repository URL and commit SHA are required") + } + + body := knownTestsRequest{ + Data: knownTestsRequestHeader{ + ID: c.id, + Type: knownTestsRequestType, + Attributes: KnownTestsRequestData{ + Service: c.serviceName, + Env: c.environment, + RepositoryURL: c.repositoryURL, + Configurations: c.testConfigurations, + }, + }, + } + + request := c.getPostRequestConfig(knownTestsURLPath, body) + if request.Compressed { + telemetry.KnownTestsRequest(telemetry.CompressedRequestCompressedType) + } else { + telemetry.KnownTestsRequest(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) + telemetry.KnownTestsRequestMs(float64(time.Since(startTime).Milliseconds())) + + if err != nil { + telemetry.KnownTestsRequestErrors(telemetry.NetworkErrorType) + return nil, fmt.Errorf("sending known tests request: %s", err.Error()) + } + + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.KnownTestsRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + if response.Compressed { + telemetry.KnownTestsResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) + } else { + telemetry.KnownTestsResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) + } + + var responseObject knownTestsResponse + err = response.Unmarshal(&responseObject) + if err != nil { + return nil, fmt.Errorf("unmarshalling known tests response: %s", err.Error()) + } + + testCount := 0 + if responseObject.Data.Attributes.Tests != nil { + for _, suites := range responseObject.Data.Attributes.Tests { + if suites == nil { + continue + } + for _, tests := range suites { + testCount += len(tests) + } + } + } + telemetry.KnownTestsResponseTests(float64(testCount)) + return &responseObject.Data.Attributes, nil +} diff --git a/internal/civisibility/utils/net/efd_api_test.go b/internal/civisibility/utils/net/known_tests_api_test.go similarity index 77% rename from internal/civisibility/utils/net/efd_api_test.go rename to internal/civisibility/utils/net/known_tests_api_test.go index 93008d25ce..79c894883f 100644 --- a/internal/civisibility/utils/net/efd_api_test.go +++ b/internal/civisibility/utils/net/known_tests_api_test.go @@ -16,15 +16,15 @@ import ( "github.com/stretchr/testify/assert" ) -func TestEfdApiRequest(t *testing.T) { +func TestKnownTestsApiRequest(t *testing.T) { var c *client - expectedResponse := efdResponse{} + expectedResponse := knownTestsResponse{} expectedResponse.Data.Type = settingsRequestType - expectedResponse.Data.Attributes.Tests = EfdResponseDataModules{ - "MyModule1": EfdResponseDataSuites{ + expectedResponse.Data.Attributes.Tests = KnownTestsResponseDataModules{ + "MyModule1": KnownTestsResponseDataSuites{ "MySuite1": []string{"Test1", "Test2"}, }, - "MyModule2": EfdResponseDataSuites{ + "MyModule2": KnownTestsResponseDataSuites{ "MySuite2": []string{"Test3", "Test4"}, }, } @@ -37,11 +37,11 @@ func TestEfdApiRequest(t *testing.T) { } if r.Header.Get(HeaderContentType) == ContentTypeJSON { - var request efdRequest + var request knownTestsRequest json.Unmarshal(body, &request) assert.Equal(t, c.id, request.Data.ID) - assert.Equal(t, efdRequestType, request.Data.Type) - assert.Equal(t, efdURLPath, r.URL.Path[1:]) + assert.Equal(t, knownTestsRequestType, request.Data.Type) + assert.Equal(t, knownTestsURLPath, r.URL.Path[1:]) assert.Equal(t, c.environment, request.Data.Attributes.Env) assert.Equal(t, c.repositoryURL, request.Data.Attributes.RepositoryURL) assert.Equal(t, c.serviceName, request.Data.Attributes.Service) @@ -62,12 +62,12 @@ func TestEfdApiRequest(t *testing.T) { cInterface := NewClient() c = cInterface.(*client) - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, err) assert.Equal(t, expectedResponse.Data.Attributes, *efdData) } -func TestEfdApiRequestFailToUnmarshal(t *testing.T) { +func TestKnownTestsApiRequestFailToUnmarshal(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to read body", http.StatusBadRequest) })) @@ -80,13 +80,13 @@ func TestEfdApiRequestFailToUnmarshal(t *testing.T) { setCiVisibilityEnv(path, server.URL) cInterface := NewClient() - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, efdData) assert.NotNil(t, err) assert.Contains(t, err.Error(), "cannot unmarshal response") } -func TestEfdApiRequestFailToGet(t *testing.T) { +func TestKnownTestsApiRequestFailToGet(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "internal processing error", http.StatusInternalServerError) })) @@ -99,8 +99,8 @@ func TestEfdApiRequestFailToGet(t *testing.T) { setCiVisibilityEnv(path, server.URL) cInterface := NewClient() - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, efdData) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "sending early flake detection request") + assert.Contains(t, err.Error(), "sending known tests request") } diff --git a/internal/civisibility/utils/net/settings_api.go b/internal/civisibility/utils/net/settings_api.go index d0b927bb6d..205d06e737 100644 --- a/internal/civisibility/utils/net/settings_api.go +++ b/internal/civisibility/utils/net/settings_api.go @@ -62,6 +62,7 @@ type ( ItrEnabled bool `json:"itr_enabled"` RequireGit bool `json:"require_git"` TestsSkipping bool `json:"tests_skipping"` + KnownTestsEnabled bool `json:"known_tests_enabled"` } ) diff --git a/internal/civisibility/utils/telemetry/telemetry_count.go b/internal/civisibility/utils/telemetry/telemetry_count.go index 337f47f92d..0e1bfaa931 100644 --- a/internal/civisibility/utils/telemetry/telemetry_count.go +++ b/internal/civisibility/utils/telemetry/telemetry_count.go @@ -199,14 +199,14 @@ func CodeCoverageErrors() { telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.errors", 1.0, nil, true) } -// EarlyFlakeDetectionRequest the number of requests sent to the early flake detection endpoint, tagged by the request compressed type. -func EarlyFlakeDetectionRequest(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request", 1.0, removeEmptyStrings([]string{ +// KnownTestsRequest the number of requests sent to the known tests endpoint, tagged by the request compressed type. +func KnownTestsRequest(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request", 1.0, removeEmptyStrings([]string{ string(requestCompressed), }), true) } -// EarlyFlakeDetectionRequestErrors the number of requests sent to the early flake detection endpoint that errored, tagged by the error type. -func EarlyFlakeDetectionRequestErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request_errors", 1.0, removeEmptyStrings(errorType), true) +// KnownTestsRequestErrors the number of requests sent to the known tests endpoint that errored, tagged by the error type. +func KnownTestsRequestErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request_errors", 1.0, removeEmptyStrings(errorType), true) } diff --git a/internal/civisibility/utils/telemetry/telemetry_distribution.go b/internal/civisibility/utils/telemetry/telemetry_distribution.go index 3b4d8d54fe..20c5786eb0 100644 --- a/internal/civisibility/utils/telemetry/telemetry_distribution.go +++ b/internal/civisibility/utils/telemetry/telemetry_distribution.go @@ -86,19 +86,19 @@ func CodeCoverageFiles(value float64) { telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "code_coverage.files", value, nil, true) } -// EarlyFlakeDetectionRequestMs records the time it takes to get the response of the early flake detection endpoint request in ms by CI Visibility. -func EarlyFlakeDetectionRequestMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.request_ms", value, nil, true) +// KnownTestsRequestMs records the time it takes to get the response of the known tests endpoint request in ms by CI Visibility. +func KnownTestsRequestMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.request_ms", value, nil, true) } -// EarlyFlakeDetectionResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. -func EarlyFlakeDetectionResponseBytes(responseCompressedType ResponseCompressedType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_bytes", value, removeEmptyStrings([]string{ +// KnownTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. +func KnownTestsResponseBytes(responseCompressedType ResponseCompressedType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_bytes", value, removeEmptyStrings([]string{ (string)(responseCompressedType), }), true) } -// EarlyFlakeDetectionResponseTests records the number of tests in the response of the early flake detection endpoint by CI Visibility. -func EarlyFlakeDetectionResponseTests(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_tests", value, nil, true) +// KnownTestsResponseTests records the number of tests in the response of the known tests endpoint by CI Visibility. +func KnownTestsResponseTests(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_tests", value, nil, true) } diff --git a/internal/container_linux.go b/internal/container_linux.go index 237c293e21..6ac57029f9 100644 --- a/internal/container_linux.go +++ b/internal/container_linux.go @@ -48,7 +48,7 @@ var ( // containerID is the containerID read at init from /proc/self/cgroup containerID string - // entityID is the entityID to use for the container. It is the `cid-` if the container id available, + // entityID is the entityID to use for the container. It is the `ci-` if the container id available, // otherwise the cgroup node controller's inode prefixed with `in-` or an empty string on incompatible OS. // We use the memory controller on cgroupv1 and the root cgroup on cgroupv2. entityID string @@ -151,7 +151,7 @@ func readEntityID(mountPath, cgroupPath string, isHostCgroupNamespace bool) stri // First try to emit the containerID if available. It will be retrieved if the container is // running in the host cgroup namespace, independently of the cgroup version. if containerID != "" { - return "cid-" + containerID + return "ci-" + containerID } // Rely on the inode if we're not running in the host cgroup namespace. if isHostCgroupNamespace { @@ -161,7 +161,7 @@ func readEntityID(mountPath, cgroupPath string, isHostCgroupNamespace bool) stri } // EntityID attempts to return the container ID or the cgroup node controller's inode if the container ID -// is not available. The cid is prefixed with `cid-` and the inode with `in-`. +// is not available. The cid is prefixed with `ci-` and the inode with `in-`. func EntityID() string { return entityID } diff --git a/internal/container_linux_test.go b/internal/container_linux_test.go index 029ff647c4..83796086f7 100644 --- a/internal/container_linux_test.go +++ b/internal/container_linux_test.go @@ -93,7 +93,7 @@ func TestReadEntityIDPrioritizeCID(t *testing.T) { containerID = "fakeContainerID" eid := readEntityID("", "", true) - assert.Equal(t, "cid-fakeContainerID", eid) + assert.Equal(t, "ci-fakeContainerID", eid) } func TestReadEntityIDFallbackOnInode(t *testing.T) { diff --git a/internal/container_stub.go b/internal/container_stub.go index c6c2487406..38f4e5ce21 100644 --- a/internal/container_stub.go +++ b/internal/container_stub.go @@ -13,7 +13,7 @@ func ContainerID() string { } // EntityID attempts to return the container ID or the cgroup v2 node inode if the container ID is not available. -// The cid is prefixed with `cid-` and the inode with `in-`. +// The cid is prefixed with `ci-` and the inode with `in-`. func EntityID() string { return "" } diff --git a/internal/env.go b/internal/env.go index 2e760526ac..ace2bba238 100644 --- a/internal/env.go +++ b/internal/env.go @@ -133,3 +133,8 @@ func BoolVal(val string, def bool) bool { } return v } + +// ExternalEnvironment returns the value of the DD_EXTERNAL_ENV environment variable. +func ExternalEnvironment() string { + return os.Getenv("DD_EXTERNAL_ENV") +} diff --git a/internal/exectracetest/go.mod b/internal/exectracetest/go.mod index d738bc656d..abb0e80313 100644 --- a/internal/exectracetest/go.mod +++ b/internal/exectracetest/go.mod @@ -5,7 +5,7 @@ go 1.22.10 require ( github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b github.com/mattn/go-sqlite3 v1.14.18 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 gopkg.in/DataDog/dd-trace-go.v1 v1.70.3 ) diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index 9aab319c37..e853c046cb 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -233,8 +233,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/internal/orchestrion/_integration/aws.v2/base.go b/internal/orchestrion/_integration/aws.v2/base.go index 8f7af1872b..8d61a26240 100644 --- a/internal/orchestrion/_integration/aws.v2/base.go +++ b/internal/orchestrion/_integration/aws.v2/base.go @@ -9,7 +9,6 @@ package awsv2 import ( "context" - "fmt" "testing" "github.com/DataDog/dd-trace-go/internal/orchestrion/_integration/internal/containers" @@ -67,7 +66,7 @@ func (b *base) expectedTraces() trace.Traces { Meta: map[string]string{ "http.method": "POST", "http.status_code": "200", - "http.url": fmt.Sprintf("http://localhost:%s/", b.port), + "http.url": "/", "network.destination.name": "localhost", "component": "net/http", "span.kind": "client", diff --git a/internal/orchestrion/_integration/go.mod b/internal/orchestrion/_integration/go.mod index daacaec5d0..6a77e05d03 100644 --- a/internal/orchestrion/_integration/go.mod +++ b/internal/orchestrion/_integration/go.mod @@ -8,7 +8,7 @@ require ( cloud.google.com/go/pubsub v1.44.0 github.com/99designs/gqlgen v0.17.62 github.com/DataDog/go-libddwaf/v3 v3.5.1 - github.com/DataDog/orchestrion v1.0.3-rc.1.0.20250120134012-8c2a6e2c1d70 + github.com/DataDog/orchestrion v1.0.4-0.20250203104009-f137fbec82f2 github.com/IBM/sarama v1.44.0 github.com/Shopify/sarama v1.38.1 github.com/aws/aws-sdk-go v1.55.5 @@ -63,7 +63,7 @@ require ( google.golang.org/api v0.213.0 google.golang.org/grpc v1.69.2 google.golang.org/grpc/examples v0.0.0-20250110041721-2d4daf347590 - gopkg.in/DataDog/dd-trace-go.v1 v1.71.0-rc.4 + gopkg.in/DataDog/dd-trace-go.v1 v1.71.0 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 gotest.tools/v3 v3.5.1 @@ -124,7 +124,7 @@ require ( github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/lipgloss v1.0.0 // indirect - github.com/charmbracelet/x/ansi v0.6.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect @@ -212,7 +212,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/highwayhash v1.0.3 // indirect @@ -230,7 +230,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/jwt/v2 v2.7.3 // indirect - github.com/nats-io/nats-server/v2 v2.10.24 // indirect + github.com/nats-io/nats-server/v2 v2.10.25 // indirect github.com/nats-io/nats.go v1.38.0 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect @@ -291,16 +291,16 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.13.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.28.0 // indirect + golang.org/x/time v0.10.0 // indirect + golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect diff --git a/internal/orchestrion/_integration/go.sum b/internal/orchestrion/_integration/go.sum index b1520d01c8..ea9d12d1d1 100644 --- a/internal/orchestrion/_integration/go.sum +++ b/internal/orchestrion/_integration/go.sum @@ -665,8 +665,8 @@ github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/ github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.22.0 h1:yfk2cF8Bx98fSFpGrehEHh1FRqewfxcCTAbUDt5r3F8= github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.22.0/go.mod h1:9qzpnBSxSOnKzbF/uHket3SSlQihQHix/ZRC2nZUUYQ= -github.com/DataDog/orchestrion v1.0.3-rc.1.0.20250120134012-8c2a6e2c1d70 h1:hmG/mImXp4R/0JzdMPw0wTv8gg/nFCSfRF5TJladt0k= -github.com/DataDog/orchestrion v1.0.3-rc.1.0.20250120134012-8c2a6e2c1d70/go.mod h1:sC0SM1AIkyex4/FZL7niB7j8hCnPza83eYOe9HPBFds= +github.com/DataDog/orchestrion v1.0.4-0.20250203104009-f137fbec82f2 h1:qYnMF6aZy/tHRvNKsPR4l+ptigLS+yE/UPl7wEjGeoQ= +github.com/DataDog/orchestrion v1.0.4-0.20250203104009-f137fbec82f2/go.mod h1:n2DmO6EPGaUzdiaRxrM0Z7U8RJaIhsjh0sFtAXL8BNQ= github.com/DataDog/sketches-go v1.4.6 h1:acd5fb+QdUzGrosfNLwrIhqyrbMORpvBy7mE+vHlT3I= github.com/DataDog/sketches-go v1.4.6/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= @@ -799,8 +799,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= -github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= -github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -1344,8 +1344,9 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -1418,8 +1419,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= -github.com/nats-io/nats-server/v2 v2.10.24 h1:KcqqQAD0ZZcG4yLxtvSFJY7CYKVYlnlWoAiVZ6i/IY4= -github.com/nats-io/nats-server/v2 v2.10.24/go.mod h1:olvKt8E5ZlnjyqBGbAXtxvSQKsPodISK5Eo/euIta4s= +github.com/nats-io/nats-server/v2 v2.10.25 h1:J0GWLDDXo5HId7ti/lTmBfs+lzhmu8RPkoKl0eSCqwc= +github.com/nats-io/nats-server/v2 v2.10.25/go.mod h1:/YYYQO7cuoOBt+A7/8cVjuhWTaTUEAlZbJT+3sMAfFU= github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= @@ -1770,8 +1771,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1787,8 +1788,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1831,8 +1832,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1894,8 +1895,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1943,8 +1944,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2033,8 +2034,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -2043,8 +2044,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2069,8 +2070,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2135,8 +2136,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/orchestrion/_integration/gorm/gorm.go b/internal/orchestrion/_integration/gorm/gorm.go index 0b3020f95c..6601824a87 100644 --- a/internal/orchestrion/_integration/gorm/gorm.go +++ b/internal/orchestrion/_integration/gorm/gorm.go @@ -23,7 +23,7 @@ type TestCase struct { func (tc *TestCase) Setup(_ context.Context, t *testing.T) { var err error - tc.DB, err = gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) + tc.DB, err = gorm.Open(sqlite.Open("file::memory:")) require.NoError(t, err) require.NoError(t, tc.DB.AutoMigrate(&Note{})) @@ -63,6 +63,21 @@ func (*TestCase) ExpectedTraces() trace.Traces { Meta: map[string]string{ "component": "gorm.io/gorm.v1", }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "resource": "SELECT * FROM `notes` WHERE user_id = ? AND `notes`.`deleted_at` IS NULL ORDER BY `notes`.`id` LIMIT 1", + "type": "sql", + "name": "sqlite3.query", + "service": "sqlite3.db", + }, + Meta: map[string]string{ + "component": "database/sql", + "span.kind": "client", + "db.system": "other_sql", + }, + }, + }, }, }, }, diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 033b2e277a..45cd4172a3 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "runtime" "sort" "sync" "testing" @@ -18,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/internal" logging "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -439,11 +441,18 @@ func Test_heartbeatInterval(t *testing.T) { } func TestNoEmptyHeaders(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping test on non-linux OS") + } + if internal.EntityID() == "" || internal.ContainerID() == "" { + t.Skip("skipping test when entity ID and container ID are not available") + } c := &client{} req := c.newRequest(RequestTypeAppStarted) assertNotEmpty := func(header string) { headers := *req.Header vals := headers[header] + assert.Greater(t, len(vals), 0, "header %s should not be empty", header) for _, v := range vals { assert.NotEmpty(t, v, "%s header should not be empty", header) } diff --git a/internal/version/version.go b/internal/version/version.go index c91fed8dcc..b5e22f6289 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -13,7 +13,7 @@ import ( // Tag specifies the current release tag. It needs to be manually // updated. A test checks that the value of Tag never points to a // git tag that is older than HEAD. -const Tag = "v1.72.0-dev" +const Tag = "v1.73.0-dev" // Dissected version number. Filled during init() var (