Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: DataDog/dd-trace-go
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.52.0
Choose a base ref
...
head repository: DataDog/dd-trace-go
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.53.0
Choose a head ref

Commits on Jun 12, 2023

  1. Copy the full SHA
    08952ee View commit details
  2. contrib/aws: fix sns nil pointer dereference (#2025)

    Co-authored-by: Zarir Hamza <zarir.hamza@datadoghq.com>
    satorunooshie and zarirhamza authored Jun 12, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b9a86a0 View commit details

Commits on Jun 14, 2023

  1. ddtrace/tracer: implement peer.service (#1975)

    Co-authored-by: Kyle Nusbaum <kyle@datadog.com>
    rarguelloF and knusbaum authored Jun 14, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    087eb5a View commit details
  2. internal/namingschema: add config flag to disable default integration…

    … service names (#2007)
    
    Co-authored-by: Andrew Glaude <andrew.glaude@datadoghq.com>
    rarguelloF and ajgajg1134 authored Jun 14, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7af5011 View commit details

Commits on Jun 15, 2023

  1. tracer: fix telemetry metrics to align with spec (#2049)

    Changes the metrics otel.spans_created and tracer_init_time to meet the telemetry naming specification.
    - otel.spans_created changes to spans_created with otel as a tag instead
    - tracer_init_time changes to init_time, and moves to a new "general" namespace
    katiehockman authored Jun 15, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b34590b View commit details

Commits on Jun 19, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    61c3018 View commit details

Commits on Jun 20, 2023

  1. contrib/google.golang.org/grpc: Fallback to dynamic service names if …

    …no global is found (#2051)
    
    Co-authored-by: Peter Kalmakis <peter.kalmakis@gmail.com>
    Co-authored-by: Katie Hockman <katie@hockman.dev>
    Co-authored-by: Diana Shevchenko <40775148+dianashevchenko@users.noreply.github.com>
    4 people authored Jun 20, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    254acfa View commit details

Commits on Jun 21, 2023

  1. Copy the full SHA
    39472a6 View commit details
  2. Copy the full SHA
    d95fdb5 View commit details

Commits on Jun 22, 2023

  1. ddtrace/tracer: Fix TestSpanTracePushSeveral (#2062)

    Start spans in this test instead of creating them manually.
    ajgajg1134 authored Jun 22, 2023
    Copy the full SHA
    d2cd391 View commit details

Commits on Jun 23, 2023

  1. Copy the full SHA
    3ca9166 View commit details

Commits on Jun 26, 2023

  1. Copy the full SHA
    1330a36 View commit details
  2. contrib/database/sql: properly annotate DB operations with execution …

    …trace tasks (#2060)
    
    After #336, spans for database operations are created after the operation.
    This is necessary because some operations may return ErrSkip if they aren't
    supported, and creating spans with those errors is very noisy. But because of
    this, execution trace tasks associated with those spans would appear to be only
    a few microseconds long since they are created alongside the span and thus only
    cover the very end of the operation.
    
    This commit creates execution trace tasks (if the execution tracer is enabled)
    before the operations, and provides a way of communicating to the APM tracer
    that it doesn't need to create a task for the span. The APM tracer will still
    annotate the task with the span ID even if a task was already created.
    nsrip-dd authored Jun 26, 2023
    Copy the full SHA
    ea87357 View commit details
  3. Copy the full SHA
    0348452 View commit details
  4. Copy the full SHA
    f9a84b1 View commit details
  5. all: run go mod tidy (#2073)

    Missed in #2060 after adding a new dependency
    nsrip-dd authored Jun 26, 2023
    Copy the full SHA
    ed9cc9c View commit details
  6. tracer: request Headers as tags for web integrations (#1764)

    Makes it possible to set request headers as tags at the integration level and at the global tracer level.
    mtoffl01 authored Jun 26, 2023
    Copy the full SHA
    1e3868b View commit details
  7. Copy the full SHA
    1170127 View commit details

Commits on Jun 27, 2023

  1. Copy the full SHA
    12ed8da View commit details
  2. Copy the full SHA
    ea1655a View commit details
  3. Copy the full SHA
    9b7defb View commit details
  4. Copy the full SHA
    b72b941 View commit details
  5. Copy the full SHA
    8dddd8b View commit details
  6. ddtrace/opentelemetry: improve TestSpanContextWithStartOptions (#2063)

    Adds a check to the TestSpanContextWithStartOptions OTel test which verifies that the payload contains two spans
    katiehockman authored Jun 27, 2023
    Copy the full SHA
    cf69fbb View commit details

Commits on Jun 28, 2023

  1. Copy the full SHA
    e7415ef View commit details
  2. contrib/aws/aws-sdk-go-v2: add WithErrorCheck option (#2042)

    Adds an WithErrorCheck function similar to #1682
    johanneswuerbach authored Jun 28, 2023
    Copy the full SHA
    0c67903 View commit details

Commits on Jun 29, 2023

  1. internal/appsec: remove unnecessary remoteconfig updates specificatio…

    …ns (#2084)
    
    Co-authored-by: Julio Guerra <julio@datadog.com>
    Hellzy and Julio-Guerra authored Jun 29, 2023
    Copy the full SHA
    ac8b4aa View commit details
  2. Copy the full SHA
    e5d2d28 View commit details
  3. Copy the full SHA
    0ec3635 View commit details
  4. Copy the full SHA
    f023ebd View commit details
  5. Added WithErrorCheck option to redis (#2040)

    Co-authored-by: Andrew Glaude <andrew.glaude@datadoghq.com>
    Co-authored-by: Andrew Glaude <ajgajg1134@gmail.com>
    3 people authored Jun 29, 2023
    Copy the full SHA
    5ea746b View commit details
  6. Copy the full SHA
    43a3523 View commit details

Commits on Jul 3, 2023

  1. contrib/emicklei/go-restful/v3: add integration for newest version (#…

    …2088)
    
    Adds support for the latest version of go-restful
    michaelbeaumont authored Jul 3, 2023
    Copy the full SHA
    e7bce49 View commit details
  2. Copy the full SHA
    2c7a8f4 View commit details

Commits on Jul 5, 2023

  1. Copy the full SHA
    c10c902 View commit details
  2. Copy the full SHA
    7f9ab67 View commit details

Commits on Jul 6, 2023

  1. appsec: rework actions system (#2044)

    Co-authored-by: Julio Guerra <julio@datadog.com>
    Hellzy and Julio-Guerra authored Jul 6, 2023
    Copy the full SHA
    2eee7a0 View commit details
  2. contrib/emicklei/go-restful.v3: move to the correct folder (#2092)

    Co-authored-by: Katie Hockman <katie@hockman.dev>
    rarguelloF and katiehockman authored Jul 6, 2023
    Copy the full SHA
    f0e9c3f View commit details
  3. Copy the full SHA
    46268f1 View commit details

Commits on Jul 10, 2023

  1. Copy the full SHA
    eaa593d View commit details

Commits on Jul 11, 2023

  1. contrib/gocql: add WithCustomTag option (#2105)

    Co-authored-by: Kyle Nusbaum <kyle@datadog.com>
    rarguelloF and knusbaum authored Jul 11, 2023
    Copy the full SHA
    60cbd27 View commit details
  2. Copy the full SHA
    2909e64 View commit details
  3. Copy the full SHA
    fecca88 View commit details

Commits on Jul 18, 2023

  1. go.mod: update go-libddwaf to v1.4.1 (#2120)

    Signed-off-by: Eliott Bouhana <eliott.bouhana@datadoghq.com>
    Julio-Guerra authored Jul 18, 2023
    Copy the full SHA
    4102a4c View commit details
  2. Copy the full SHA
    ac23d5a View commit details
  3. Copy the full SHA
    f905f70 View commit details

Commits on Jul 19, 2023

  1. internal: Improve the Iter performance of LockMap on empty maps (#2127)

    Turns out doing RLock and RUnlock can still be slow especially when this code is in a HOT path. For most users this map will be empty. Therefore it's worth speeding up that case at the cost of a tiny bit of extra memory and a bit of complexity.
    ajgajg1134 authored Jul 19, 2023
    Copy the full SHA
    3eabc50 View commit details
  2. contrib/internal/httptrace: improve performance of span starts (#2128)

    This commit improves the performance of starting a span by reducing work
    done, especially allocations.
    knusbaum authored Jul 19, 2023
    Copy the full SHA
    216e947 View commit details
  3. Copy the full SHA
    a5f2e0d View commit details

Commits on Jul 20, 2023

  1. contrib/emicklei/go-restful.v3: fix componentName (cherry-pick #2132) (

    …#2136)
    
    After #2092, where the integration was moved to the correct folder, I forgot to update the componentName as well.
    
    This is a cherry-pick of #2132 onto the v1.53.x release branch.
    knusbaum authored Jul 20, 2023
    Copy the full SHA
    8615a7e View commit details
Showing with 4,705 additions and 1,041 deletions.
  1. +35 −0 .github/actions/setup-go/action.yml
  2. +2 −2 .github/workflows/appsec.yml
  3. +1 −1 .github/workflows/govulncheck.yml
  4. +3 −3 .github/workflows/multios-unit-tests.yml
  5. +14 −22 .github/workflows/parametric-tests.yml
  6. +18 −9 .github/workflows/system-tests.yml
  7. +67 −70 .github/workflows/unit-integration-tests.yml
  8. +1 −0 .gitlab-ci.yml
  9. +3 −2 CONTRIBUTING.md
  10. +2 −2 contrib/Shopify/sarama/sarama.go
  11. +27 −14 contrib/aws/aws-sdk-go-v2/aws/aws.go
  12. +90 −7 contrib/aws/aws-sdk-go-v2/aws/aws_test.go
  13. +10 −0 contrib/aws/aws-sdk-go-v2/aws/option.go
  14. +2 −2 contrib/cloud.google.com/go/pubsub.v1/pubsub.go
  15. +2 −2 contrib/confluentinc/confluent-kafka-go/kafka/kafka.go
  16. +18 −0 contrib/database/sql/conn.go
  17. +161 −0 contrib/database/sql/exec_trace_test.go
  18. +26 −0 contrib/database/sql/internal/mockdriver.go
  19. +2 −0 contrib/database/sql/sql.go
  20. +11 −1 contrib/database/sql/stmt.go
  21. +29 −2 contrib/database/sql/tx.go
  22. +56 −0 contrib/emicklei/go-restful.v3/example_test.go
  23. +83 −0 contrib/emicklei/go-restful.v3/option.go
  24. +54 −0 contrib/emicklei/go-restful.v3/restful.go
  25. +324 −0 contrib/emicklei/go-restful.v3/restful_test.go
  26. +14 −0 contrib/emicklei/go-restful/option.go
  27. +1 −0 contrib/emicklei/go-restful/restful.go
  28. +100 −0 contrib/emicklei/go-restful/restful_test.go
  29. +1 −1 contrib/gin-gonic/gin/gintrace.go
  30. +89 −0 contrib/gin-gonic/gin/gintrace_test.go
  31. +14 −0 contrib/gin-gonic/gin/option.go
  32. +1 −0 contrib/go-chi/chi.v5/chi.go
  33. +92 −0 contrib/go-chi/chi.v5/chi_test.go
  34. +14 −0 contrib/go-chi/chi.v5/option.go
  35. +1 −0 contrib/go-chi/chi/chi.go
  36. +93 −0 contrib/go-chi/chi/chi_test.go
  37. +14 −0 contrib/go-chi/chi/option.go
  38. +10 −0 contrib/go-redis/redis.v7/option.go
  39. +2 −2 contrib/go-redis/redis.v7/redis.go
  40. +35 −0 contrib/go-redis/redis.v7/redis_test.go
  41. +10 −0 contrib/go-redis/redis.v8/option.go
  42. +2 −2 contrib/go-redis/redis.v8/redis.go
  43. +33 −0 contrib/go-redis/redis.v8/redis_test.go
  44. +6 −8 contrib/gocql/gocql/example_test.go
  45. +115 −20 contrib/gocql/gocql/gocql.go
  46. +152 −11 contrib/gocql/gocql/gocql_test.go
  47. +14 −1 contrib/gocql/gocql/option.go
  48. +38 −9 contrib/google.golang.org/grpc/appsec.go
  49. +2 −2 contrib/google.golang.org/grpc/grpc.go
  50. +86 −0 contrib/google.golang.org/grpc/grpc_test.go
  51. +17 −4 contrib/google.golang.org/grpc/option.go
  52. +2 −15 contrib/gorilla/mux/mux.go
  53. +81 −12 contrib/gorilla/mux/mux_test.go
  54. +11 −8 contrib/gorilla/mux/option.go
  55. +51 −20 contrib/internal/httptrace/httptrace.go
  56. +49 −0 contrib/internal/httptrace/httptrace_test.go
  57. +2 −3 contrib/labstack/echo.v4/appsec.go
  58. +1 −1 contrib/labstack/echo.v4/echotrace.go
  59. +93 −0 contrib/labstack/echo.v4/echotrace_test.go
  60. +16 −0 contrib/labstack/echo.v4/option.go
  61. +1 −1 contrib/labstack/echo/echotrace.go
  62. +93 −0 contrib/labstack/echo/echotrace_test.go
  63. +15 −0 contrib/labstack/echo/option.go
  64. +3 −2 contrib/net/http/http.go
  65. +110 −0 contrib/net/http/http_test.go
  66. +24 −0 contrib/net/http/option.go
  67. +7 −5 contrib/net/http/roundtripper.go
  68. +30 −0 contrib/net/http/roundtripper_test.go
  69. +10 −0 contrib/redis/go-redis.v9/option.go
  70. +2 −2 contrib/redis/go-redis.v9/redis.go
  71. +31 −0 contrib/redis/go-redis.v9/redis_test.go
  72. +2 −2 contrib/segmentio/kafka.go.v0/kafka.go
  73. +1 −0 contrib/urfave/negroni/negroni.go
  74. +93 −0 contrib/urfave/negroni/negroni_test.go
  75. +14 −0 contrib/urfave/negroni/option.go
  76. +0 −31 ddtrace/ext/cassandra.go
  77. +32 −3 ddtrace/ext/db.go
  78. +25 −0 ddtrace/ext/messaging.go
  79. +0 −12 ddtrace/ext/tags.go
  80. +3 −0 ddtrace/opentelemetry/span_test.go
  81. +3 −1 ddtrace/opentelemetry/tracer.go
  82. +1 −1 ddtrace/opentelemetry/tracer_test.go
  83. +3 −1 ddtrace/opentracer/tracer.go
  84. +1 −1 ddtrace/opentracer/tracer_test.go
  85. +75 −0 ddtrace/tracer/option.go
  86. +121 −0 ddtrace/tracer/option_test.go
  87. +14 −3 ddtrace/tracer/span.go
  88. +26 −2 ddtrace/tracer/span_test.go
  89. +97 −9 ddtrace/tracer/spancontext.go
  90. +223 −6 ddtrace/tracer/spancontext_test.go
  91. +3 −3 ddtrace/tracer/telemetry_test.go
  92. +18 −3 ddtrace/tracer/textmap.go
  93. +64 −2 ddtrace/tracer/textmap_test.go
  94. +19 −3 ddtrace/tracer/tracer.go
  95. +18 −11 go.mod
  96. +38 −23 go.sum
  97. +38 −4 internal/appsec/appsec.go
  98. +2 −1 internal/appsec/appsec_test.go
  99. +3 −3 internal/appsec/config.go
  100. +0 −73 internal/appsec/dyngo/instrumentation/grpcsec/actions.go
  101. +29 −1 internal/appsec/dyngo/instrumentation/grpcsec/grpc.go
  102. +0 −112 internal/appsec/dyngo/instrumentation/httpsec/actions.go
  103. +43 −98 internal/appsec/dyngo/instrumentation/httpsec/http.go
  104. +135 −0 internal/appsec/dyngo/instrumentation/sharedsec/actions.go
  105. +52 −4 internal/appsec/dyngo/instrumentation/{httpsec → sharedsec}/actions_test.go
  106. 0 internal/appsec/dyngo/instrumentation/{httpsec → sharedsec}/blocked-template.html
  107. 0 internal/appsec/dyngo/instrumentation/{httpsec → sharedsec}/blocked-template.json
  108. +15 −15 internal/appsec/dyngo/instrumentation/sharedsec/shared.go
  109. +88 −0 internal/appsec/dyngo/operation.go
  110. +45 −0 internal/appsec/dyngo/operation_test.go
  111. +5 −12 internal/appsec/remoteconfig.go
  112. +35 −37 internal/appsec/remoteconfig_test.go
  113. +7 −2 internal/appsec/rule_test.go
  114. +19 −58 internal/appsec/rules_manager.go
  115. +0 −160 internal/appsec/rules_manager_test.go
  116. +36 −0 internal/appsec/testdata/user_rules.json
  117. +90 −38 internal/appsec/waf.go
  118. +29 −13 internal/appsec/waf_test.go
  119. +1 −1 internal/appsec/waf_unit_test.go
  120. +32 −1 internal/globalconfig/globalconfig.go
  121. +20 −0 internal/globalconfig/globalconfig_test.go
  122. +27 −7 internal/namingschema/namingschema.go
  123. +12 −7 internal/namingschema/service_name.go
  124. +38 −6 internal/namingschema/service_name_test.go
  125. +50 −0 internal/normalizer/normalizer.go
  126. +86 −0 internal/normalizer/normalizer_test.go
  127. +2 −0 internal/remoteconfig/remoteconfig.go
  128. +2 −0 internal/telemetry/message.go
  129. +1 −1 internal/telemetry/telemetry.go
  130. +47 −0 internal/trace_context.go
  131. +25 −0 internal/trace_context_test.go
  132. +35 −0 internal/traceprof/profiler.go
  133. +64 −0 internal/utils.go
  134. +65 −0 internal/utils_test.go
  135. +1 −1 internal/version/version.go
  136. +1 −1 profiler/options.go
  137. +2 −0 profiler/profiler.go
35 changes: 35 additions & 0 deletions .github/actions/setup-go/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: 'Setup Go'
description: 'Setup Go and gotestsum for running tests.'
inputs:
go-version:
required: true
description: Go version to setup.
runs:
using: "composite"
steps:
- uses: actions/setup-go@v4
with:
go-version: ${{ inputs.go-version }}
cache: false
check-latest: true

# We're not using the cache feature provided by setup-go so that we can
# also cache the gotestsum install below. This can save up to 60s of CI
# time. It's not entirely clear why the gotestsum install step is slow
# sometimes, but from my debugging it seems hanging on some Off-CPU,
# perhaps network activity occasionally.
- name: Cache go
id: cache-go
uses: actions/cache@v3
with:
path: |
/home/runner/.cache/go-build
/home/runner/go/pkg/mod
/home/runner/go/bin
key: ${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.mod', '**/go.sum') }}

- name: Install gotestsum
if: steps.cache-go.outputs.cache-hit != 'true'
shell: bash
run: |
go install gotest.tools/gotestsum@latest
4 changes: 2 additions & 2 deletions .github/workflows/appsec.yml
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ jobs:
# Install gotestsum to get the results in a junit file
env GOBIN=$PWD go install gotest.tools/gotestsum@latest
# Run the tests with gotestsum
env CGO_ENABLED=${{ matrix.cgo_enabled }} DD_APPSEC_ENABLED=${{ matrix.appsec_enabled }} ./gotestsum --junitfile $JUNIT_REPORT -- -v ${{ matrix.build_tags != '' && format('-tags="{0}"', matrix.build_tags) || ''}} $TO_TEST || true
env CGO_ENABLED=${{ matrix.cgo_enabled }} DD_APPSEC_ENABLED=${{ matrix.appsec_enabled }} ./gotestsum --junitfile $JUNIT_REPORT -- -v -tags="${{matrix.build_tags}}" $TO_TEST
- name: Upload the results to Datadog CI App
uses: ./.github/actions/dd-ci-upload
@@ -105,7 +105,7 @@ jobs:
# Install gotestsum to get the results in a junit file
env GOBIN=$PWD go install gotest.tools/gotestsum@latest
# Run the tests with gotestsum
env CGO_ENABLED=${{ matrix.cgo_enabled }} ${{ matrix.appsec_enabled }} ./gotestsum --junitfile $JUNIT_REPORT -- -v ${{ matrix.build_tags != '' && format('-tags="{0}"', matrix.build_tags) || ''}} $TO_TEST || true
env CGO_ENABLED=${{ matrix.cgo_enabled }} DD_APPSEC_ENABLED=${{ matrix.appsec_enabled }} ./gotestsum --junitfile $JUNIT_REPORT -- -v -tags="${{matrix.build_tags}}" $TO_TEST
- name: Upload the results to Datadog CI App
if: matrix.distribution != 'alpine' # datadog-ci CLI doesn't work on alpine
2 changes: 1 addition & 1 deletion .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
@@ -23,4 +23,4 @@ jobs:
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: govulncheck -v -tags appsec ./ddtrace/... ./appsec/... ./profiler/... ./internal/...
run: govulncheck -tags appsec ./ddtrace/... ./appsec/... ./profiler/... ./internal/...
6 changes: 3 additions & 3 deletions .github/workflows/multios-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ jobs:
test-multi-os:
runs-on: ${{ inputs.runs-on }}
env:
REPORT: gotestsum-report.xml # path to where test results will be saved
REPORT: gotestsum-report.xml # path to where test results will be saved
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -41,8 +41,8 @@ jobs:
- name: "Runner ${{ matrix.runner-index }}: Test Core and Contrib (No Integration Tests)"
shell: bash
run: |
go list ./... | grep -v -e grpc.v12 -e google.golang.org/api -e sarama -e confluent-kafka-go -e cmemprof | sort >packages.txt
gotestsum --junitfile ${REPORT} -- $(cat packages.txt) -v -coverprofile=coverage.txt -covermode=atomic
go list ./... | grep -v -e grpc.v12 -e google.golang.org/api -e sarama -e confluent-kafka-go -e cmemprof | sort >packages.txt
gotestsum --junitfile ${REPORT} -- $(cat packages.txt) -v -coverprofile=coverage.txt -covermode=atomic -timeout 15m
- name: Upload the results to Datadog CI App
if: always()
uses: ./.github/actions/dd-ci-upload
36 changes: 14 additions & 22 deletions .github/workflows/parametric-tests.yml
Original file line number Diff line number Diff line change
@@ -18,41 +18,33 @@ on:
jobs:
parametric-tests:
if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'DataDog/dd-trace-go')
runs-on: ubuntu-latest
runs-on:
group: "APM Larger Runners"
env:
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
TEST_LIBRARY: golang
steps:
- name: Checkout system tests
uses: actions/checkout@v3
with:
repository: 'DataDog/system-tests'
path: system-tests

- name: Checkout Go
- name: Checkout dd-trace-go
uses: actions/checkout@v3
with:
path: system-tests/golang
path: utils/build/docker/golang/parametric/dd-trace-go

- uses: actions/setup-go@v3
with:
go-version: '1.18.7'
go-version: '1.18'

- name: Patch dd-trace-go version
run: |
cd system-tests/utils/build/docker/golang/parametric/
go get gopkg.in/DataDog/dd-trace-go.v1@$COMMIT_SHA
cd utils/build/docker/golang/parametric/
echo "replace gopkg.in/DataDog/dd-trace-go.v1 => ./dd-trace-go" >> go.mod
go mod tidy
- name: Checkout Python
uses: actions/checkout@v3
with:
path: system-tests/python
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install
run: |
pip install wheel
- name: Build runner
uses: ./.github/actions/install_runner

- name: Run
run: |
pip install -r system-tests/requirements.txt
cd system-tests/parametric
CLIENTS_ENABLED=golang ./run.sh
run: ./run.sh PARAMETRIC
27 changes: 18 additions & 9 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
@@ -24,6 +24,8 @@ on:
jobs:
system-tests:
if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'DataDog/dd-trace-go')
# Note: Not using large runners because the jobs spawned by this pipeline
# don't seem to get a noticable speedup from using larger runners.
runs-on: ubuntu-latest
strategy:
matrix:
@@ -40,6 +42,7 @@ jobs:
- APPSEC_REQUEST_BLOCKING
- APM_TRACING_E2E
- APM_TRACING_E2E_SINGLE_SPAN
- APM_TRACING_E2E_OTEL
include:
- weblog-variant: net-http
scenario: REMOTE_CONFIG_MOCKED_BACKEND_ASM_FEATURES
@@ -71,6 +74,11 @@ jobs:
DD_API_KEY=$SYSTEM_TESTS_E2E_DD_API_KEY
DD_APPLICATION_KEY=$SYSTEM_TESTS_E2E_DD_APP_KEY
DD_SITE="datadoghq.com"
- scenario: APM_TRACING_E2E_OTEL
env:
DD_API_KEY=$SYSTEM_TESTS_E2E_DD_API_KEY
DD_APPLICATION_KEY=$SYSTEM_TESTS_E2E_DD_APP_KEY
DD_SITE="datadoghq.com"

fail-fast: false
env:
@@ -81,24 +89,25 @@ jobs:
SYSTEM_TESTS_E2E_DD_APP_KEY: ${{ secrets.SYSTEM_TESTS_E2E_DD_APP_KEY }}
name: Test (${{ matrix.weblog-variant }}, ${{ matrix.scenario }})
steps:
- name: Setup python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'

- name: Checkout system tests
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: 'DataDog/system-tests'
ref: ${{ inputs.ref }}

- name: Checkout dd-trace-go
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: 'binaries/dd-trace-go'

- name: Build
run: ./build.sh
- name: Build weblog
run: ./build.sh -i weblog

- name: Build runner
uses: ./.github/actions/install_runner

- name: Build agent
run: ./build.sh -i agent

- name: Run
run: env ${{ matrix.env }} ./run.sh ${{ matrix.scenario }}
137 changes: 67 additions & 70 deletions .github/workflows/unit-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -12,84 +12,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
repository: 'DataDog/dd-trace-go'
uses: actions/checkout@v3

- name: Copyright
run: |
go run checkcopyright.go
lint:
runs-on: ubuntu-latest
runs-on:
group: "APM Larger Runners"
steps:
- uses: actions/setup-go@v4
- name: Checkout
uses: actions/checkout@v3

- name: Setup Go
uses: ./.github/actions/setup-go
with:
go-version: ${{ inputs.go-version }}
cache: true
- uses: actions/checkout@v3
with:
repository: 'DataDog/dd-trace-go'

- name: golangci-lint
uses: reviewdog/action-golangci-lint@v2
with:
golangci_lint_version: v1.52.2
fail_on_error: true
reporter: github-pr-review

test-core:
runs-on: ubuntu-latest
env:
TEST_RESULTS: /tmp/test-results # path to where test results will be saved
INTEGRATION: true
services:
datadog-agent:
image: datadog/agent:latest
env:
DD_HOSTNAME: "github-actions-worker"
DD_APM_ENABLED: true
DD_BIND_HOST: "0.0.0.0"
DD_API_KEY: "invalid_key_but_this_is_fine"
# We need to specify a custom health-check. By default, this container will remain "unhealthy" since
# we don't fully configure it with a valid API key (and possibly other reasons)
# This command just checks for our ability to connect to port 8126
options: >-
--health-cmd "bash -c '</dev/tcp/127.0.0.1/8126'"
ports:
- 8125:8125/udp
- 8126:8126
steps:
- name: Checkout
uses: actions/checkout@v2
with:
repository: 'DataDog/dd-trace-go'
- uses: actions/setup-go@v3
with:
go-version: ${{ inputs.go-version }}
check-latest: true
cache: true
- name: Install gotestsum
run: go install gotest.tools/gotestsum@latest
- name: Test Core
run: |
mkdir -p $TEST_RESULTS
PACKAGE_NAMES=$(go list ./... | grep -v /contrib/)
gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic
- name: Upload the results to Datadog CI App
if: always()
continue-on-error: true
uses: ./.github/actions/dd-ci-upload
with:
dd-api-key: ${{ secrets.DD_CI_API_KEY }}
files: ${{ env.TEST_RESULTS }}/gotestsum-report.xml
tags: go:${{ inputs.go-version }}},arch:${{ runner.arch }},os:${{ runner.os }},distribution:${{ runner.distribution }}
- name: Upload Coverage
if: always()
shell: bash
run: bash <(curl -s https://codecov.io/bash)

test-contrib:
runs-on: ubuntu-latest
runs-on:
group: "APM Larger Runners"
env:
TEST_RESULTS: /tmp/test-results # path to where test results will be saved
INTEGRATION: true
@@ -210,16 +160,13 @@ jobs:
- 4566:4566
steps:
- name: Checkout
uses: actions/checkout@v2
with:
repository: 'DataDog/dd-trace-go'
- uses: actions/setup-go@v3
uses: actions/checkout@v3

- name: Setup Go
uses: ./.github/actions/setup-go
with:
go-version: ${{ inputs.go-version }}
check-latest: true
cache: true
- name: Install gotestsum
run: go install gotest.tools/gotestsum@latest

- name: Test Contrib
run: |
mkdir -p $TEST_RESULTS
@@ -264,3 +211,53 @@ jobs:
git fetch origin && git checkout v1.0.0 && cd ../..
go test -mod=vendor -v ./contrib/google.golang.org/grpc.v12/...
test-core:
runs-on:
group: "APM Larger Runners"
env:
TEST_RESULTS: /tmp/test-results # path to where test results will be saved
INTEGRATION: true
services:
datadog-agent:
image: datadog/agent:latest
env:
DD_HOSTNAME: "github-actions-worker"
DD_APM_ENABLED: true
DD_BIND_HOST: "0.0.0.0"
DD_API_KEY: "invalid_key_but_this_is_fine"
# We need to specify a custom health-check. By default, this container will remain "unhealthy" since
# we don't fully configure it with a valid API key (and possibly other reasons)
# This command just checks for our ability to connect to port 8126
options: >-
--health-cmd "bash -c '</dev/tcp/127.0.0.1/8126'"
ports:
- 8125:8125/udp
- 8126:8126
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Go
uses: ./.github/actions/setup-go
with:
go-version: ${{ inputs.go-version }}

- name: Test Core
run: |
mkdir -p $TEST_RESULTS
PACKAGE_NAMES=$(go list ./... | grep -v /contrib/)
gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic
- name: Upload the results to Datadog CI App
if: always()
continue-on-error: true
uses: ./.github/actions/dd-ci-upload
with:
dd-api-key: ${{ secrets.DD_CI_API_KEY }}
files: ${{ env.TEST_RESULTS }}/gotestsum-report.xml
tags: go:${{ inputs.go-version }}},arch:${{ runner.arch }},os:${{ runner.os }},distribution:${{ runner.distribution }}
- name: Upload Coverage
if: always()
shell: bash
run: bash <(curl -s https://codecov.io/bash)
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ variables:
INDEX_FILE: index.txt
KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: dd-trace-go
FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true"
BENCHMARK_TARGETS: "BenchmarkConcurrentTracing|BenchmarkStartSpan|BenchmarkSingleSpanRetention|BenchmarkOTelApiWithCustomTags|BenchmarkInjectW3C|BenchmarkExtractW3C"

include:
- ".gitlab/benchmarks.yml"
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -66,5 +66,6 @@ git update-index --no-assume-unchanged go.*
Some benchmarks will run on any new PR commits, the results will be commented into the PR on completion.

#### Adding a new benchmark
To add additional benchmarks that should run for every PR go to `.gitlab/scripts/run-benchmarks.sh`.
Add the name of your benchmark to the list in `-bench "BenchmarkConcurrentTracing|BenchmarkStartSpan"` using pipe character separators. Note that your new benchmark must already exist in the `main` branch, for that reason it is best for new benchmarks to be added in their own PR and a second PR opened afterwards to add them to the PR benchmark script.
To add additional benchmarks that should run for every PR, go to `.gitlab-ci.yml`.
Add the name of your benchmark to the `BENCHMARK_TARGETS` variable using pipe character separators.

4 changes: 2 additions & 2 deletions contrib/Shopify/sarama/sarama.go
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ func WrapPartitionConsumer(pc sarama.PartitionConsumer, opts ...Option) sarama.P
tracer.Tag("offset", msg.Offset),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindConsumer),
tracer.Tag(ext.MessagingSystem, "kafka"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemKafka),
tracer.Measured(),
}
if !math.IsNaN(cfg.analyticsRate) {
@@ -270,7 +270,7 @@ func startProducerSpan(cfg *config, version sarama.KafkaVersion, msg *sarama.Pro
tracer.SpanType(ext.SpanTypeMessageProducer),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindProducer),
tracer.Tag(ext.MessagingSystem, "kafka"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemKafka),
}
if !math.IsNaN(cfg.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
41 changes: 27 additions & 14 deletions contrib/aws/aws-sdk-go-v2/aws/aws.go
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ const (
tagAWSRequestID = "aws.request_id"
tagQueueName = "queuename"
tagTopicName = "topicname"
tagTargetName = "targetname"
tagTableName = "tablename"
tagStreamName = "streamname"
tagBucketName = "bucketname"
@@ -126,7 +127,10 @@ func (mw *traceMiddleware) startTraceMiddleware(stack *middleware.Stack) error {

// Handle initialize and continue through the middleware chain.
out, metadata, err = next.HandleInitialize(spanctx, in)
span.Finish(tracer.WithError(err))
if err != nil && (mw.cfg.errCheck == nil || mw.cfg.errCheck(err)) {
span.SetTag(ext.Error, err)
}
span.Finish()

return out, metadata, err
}), middleware.After)
@@ -141,7 +145,7 @@ func resourceNameFromParams(requestInput middleware.InitializeInput, awsService
case "S3":
k, v = tagBucketName, bucketName(requestInput)
case "SNS":
k, v = tagTopicName, topicName(requestInput)
k, v = destinationTagValue(requestInput)
case "DynamoDB":
k, v = tagTableName, tableName(requestInput)
case "Kinesis":
@@ -193,28 +197,37 @@ func bucketName(requestInput middleware.InitializeInput) string {
return ""
}

func topicName(requestInput middleware.InitializeInput) string {
var topicArn string
func destinationTagValue(requestInput middleware.InitializeInput) (tag string, value string) {
tag = tagTopicName
var s string
switch params := requestInput.Parameters.(type) {
case *sns.PublishInput:
topicArn = *params.TopicArn
switch {
case params.TopicArn != nil:
s = *params.TopicArn
case params.TargetArn != nil:
tag = tagTargetName
s = *params.TargetArn
default:
return "destination", "empty"
}
case *sns.PublishBatchInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.GetTopicAttributesInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.ListSubscriptionsByTopicInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.RemovePermissionInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.SetTopicAttributesInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.SubscribeInput:
topicArn = *params.TopicArn
s = *params.TopicArn
case *sns.CreateTopicInput:
return *params.Name
return tag, *params.Name
}
parts := strings.Split(topicArn, ":")
return parts[len(parts)-1]
parts := strings.Split(s, ":")
return tag, parts[len(parts)-1]
}

func tableName(requestInput middleware.InitializeInput) string {
97 changes: 90 additions & 7 deletions contrib/aws/aws-sdk-go-v2/aws/aws_test.go
Original file line number Diff line number Diff line change
@@ -353,17 +353,43 @@ func TestAppendMiddlewareS3ListObjects(t *testing.T) {
func TestAppendMiddlewareSnsPublish(t *testing.T) {
tests := []struct {
name string
publishInput *sns.PublishInput
tagKey string
expectedTagValue string
responseStatus int
responseBody []byte
expectedStatusCode int
}{
{
name: "test mocked sns failure request",
name: "test mocked sns failure request",
publishInput: &sns.PublishInput{
Message: aws.String("Hello world!"),
TopicArn: aws.String("arn:aws:sns:us-east-1:111111111111:MyTopicName"),
},
tagKey: tagTopicName,
expectedTagValue: "MyTopicName",
responseStatus: 400,
expectedStatusCode: 400,
},
{
name: "test mocked sns success request",
name: "test mocked sns destination topic arn success request",
publishInput: &sns.PublishInput{
Message: aws.String("Hello world!"),
TopicArn: aws.String("arn:aws:sns:us-east-1:111111111111:MyTopicName"),
},
tagKey: tagTopicName,
expectedTagValue: "MyTopicName",
responseStatus: 200,
expectedStatusCode: 200,
},
{
name: "test mocked sns destination target arn success request",
publishInput: &sns.PublishInput{
Message: aws.String("Hello world!"),
TargetArn: aws.String("arn:aws:sns:us-east-1:111111111111:MyTargetName"),
},
tagKey: tagTargetName,
expectedTagValue: "MyTargetName",
responseStatus: 200,
expectedStatusCode: 200,
},
@@ -393,10 +419,7 @@ func TestAppendMiddlewareSnsPublish(t *testing.T) {
AppendMiddleware(&awsCfg)

snsClient := sns.NewFromConfig(awsCfg)
snsClient.Publish(context.Background(), &sns.PublishInput{
Message: aws.String("Hello world!"),
TopicArn: aws.String("arn:aws:sns:us-east-1:111111111111:MyTopicName"),
})
snsClient.Publish(context.Background(), tt.publishInput)

spans := mt.FinishedSpans()

@@ -406,7 +429,7 @@ func TestAppendMiddlewareSnsPublish(t *testing.T) {
assert.Equal(t, "Publish", s.Tag(tagAWSOperation))
assert.Equal(t, "SNS", s.Tag(tagAWSService))
assert.Equal(t, "SNS", s.Tag(tagService))
assert.Equal(t, "MyTopicName", s.Tag(tagTopicName))
assert.Equal(t, tt.expectedTagValue, s.Tag(tt.tagKey))

assert.Equal(t, "eu-west-1", s.Tag(tagAWSRegion))
assert.Equal(t, "eu-west-1", s.Tag(tagRegion))
@@ -994,3 +1017,63 @@ func repeat(s string, n int) []string {
}
return r
}

func TestWithErrorCheck(t *testing.T) {
tests := []struct {
name string
opts []Option
errExist bool
}{
{
name: "with defaults",
opts: nil,
errExist: true,
},
{
name: "with errCheck true",
opts: []Option{WithErrorCheck(func(err error) bool {
return true
})},
errExist: true,
}, {
name: "with errCheck false",
opts: []Option{WithErrorCheck(func(err error) bool {
return false
})},
errExist: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

server := mockAWS(400)
defer server.Close()

resolver := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
return aws.Endpoint{
PartitionID: "aws",
URL: server.URL,
SigningRegion: "eu-west-1",
}, nil
})

awsCfg := aws.Config{
Region: "eu-west-1",
Credentials: aws.AnonymousCredentials{},
EndpointResolver: resolver,
}

AppendMiddleware(&awsCfg, tt.opts...)

sqsClient := sqs.NewFromConfig(awsCfg)
sqsClient.ListQueues(context.Background(), &sqs.ListQueuesInput{})

spans := mt.FinishedSpans()
assert.Len(t, spans, 1)
s := spans[0]
assert.Equal(t, tt.errExist, s.Tag(ext.Error) != nil)
})
}
}
10 changes: 10 additions & 0 deletions contrib/aws/aws-sdk-go-v2/aws/option.go
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import (
type config struct {
serviceName string
analyticsRate float64
errCheck func(err error) bool
}

// Option represents an option that can be passed to Dial.
@@ -58,3 +59,12 @@ func WithAnalyticsRate(rate float64) Option {
}
}
}

// WithErrorCheck specifies a function fn which determines whether the passed
// error should be marked as an error. The fn is called whenever an aws operation
// finishes with an error.
func WithErrorCheck(fn func(err error) bool) Option {
return func(cfg *config) {
cfg.errCheck = fn
}
}
4 changes: 2 additions & 2 deletions contrib/cloud.google.com/go/pubsub.v1/pubsub.go
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ func Publish(ctx context.Context, t *pubsub.Topic, msg *pubsub.Message, opts ...
tracer.Tag("ordering_key", msg.OrderingKey),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindProducer),
tracer.Tag(ext.MessagingSystem, "googlepubsub"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemGCPPubsub),
}
if cfg.serviceName != "" {
spanOpts = append(spanOpts, tracer.ServiceName(cfg.serviceName))
@@ -108,7 +108,7 @@ func WrapReceiveHandler(s *pubsub.Subscription, f func(context.Context, *pubsub.
tracer.Tag("publish_time", msg.PublishTime.String()),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindConsumer),
tracer.Tag(ext.MessagingSystem, "googlepubsub"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemGCPPubsub),
tracer.ChildOf(parentSpanCtx),
}
if cfg.serviceName != "" {
4 changes: 2 additions & 2 deletions contrib/confluentinc/confluent-kafka-go/kafka/kafka.go
Original file line number Diff line number Diff line change
@@ -108,7 +108,7 @@ func (c *Consumer) startSpan(msg *kafka.Message) ddtrace.Span {
tracer.Tag("offset", msg.TopicPartition.Offset),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindConsumer),
tracer.Tag(ext.MessagingSystem, "kafka"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemKafka),
tracer.Measured(),
}

@@ -224,7 +224,7 @@ func (p *Producer) startSpan(msg *kafka.Message) ddtrace.Span {
tracer.SpanType(ext.SpanTypeMessageProducer),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.SpanKind, ext.SpanKindProducer),
tracer.Tag(ext.MessagingSystem, "kafka"),
tracer.Tag(ext.MessagingSystem, ext.MessagingSystemKafka),
tracer.Tag(ext.MessagingKafkaPartition, msg.TopicPartition.Partition),
}

18 changes: 18 additions & 0 deletions contrib/database/sql/conn.go
Original file line number Diff line number Diff line change
@@ -71,13 +71,17 @@ func (tc *TracedConn) WrappedConn() driver.Conn {
func (tc *TracedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
start := time.Now()
if connBeginTx, ok := tc.Conn.(driver.ConnBeginTx); ok {
ctx, end := startTraceTask(ctx, QueryTypeBegin)
defer end()
tx, err = connBeginTx.BeginTx(ctx, opts)
tc.tryTrace(ctx, QueryTypeBegin, "", start, err)
if err != nil {
return nil, err
}
return &tracedTx{tx, tc.traceParams, ctx}, nil
}
ctx, end := startTraceTask(ctx, QueryTypeBegin)
defer end()
tx, err = tc.Conn.Begin()
tc.tryTrace(ctx, QueryTypeBegin, "", start, err)
if err != nil {
@@ -103,13 +107,17 @@ func (tc *TracedConn) PrepareContext(ctx context.Context, query string) (stmt dr
}
cquery, spanID := tc.injectComments(ctx, query, mode)
if connPrepareCtx, ok := tc.Conn.(driver.ConnPrepareContext); ok {
ctx, end := startTraceTask(ctx, QueryTypePrepare)
defer end()
stmt, err := connPrepareCtx.PrepareContext(ctx, cquery)
tc.tryTrace(ctx, QueryTypePrepare, query, start, err, append(withDBMTraceInjectedTag(mode), tracer.WithSpanID(spanID))...)
if err != nil {
return nil, err
}
return &tracedStmt{Stmt: stmt, traceParams: tc.traceParams, ctx: ctx, query: query}, nil
}
ctx, end := startTraceTask(ctx, QueryTypePrepare)
defer end()
stmt, err = tc.Prepare(cquery)
tc.tryTrace(ctx, QueryTypePrepare, query, start, err, append(withDBMTraceInjectedTag(mode), tracer.WithSpanID(spanID))...)
if err != nil {
@@ -124,6 +132,8 @@ func (tc *TracedConn) ExecContext(ctx context.Context, query string, args []driv
start := time.Now()
if execContext, ok := tc.Conn.(driver.ExecerContext); ok {
cquery, spanID := tc.injectComments(ctx, query, tc.cfg.dbmPropagationMode)
ctx, end := startTraceTask(ctx, QueryTypeExec)
defer end()
r, err := execContext.ExecContext(ctx, cquery, args)
tc.tryTrace(ctx, QueryTypeExec, query, start, err, append(withDBMTraceInjectedTag(tc.cfg.dbmPropagationMode), tracer.WithSpanID(spanID))...)
return r, err
@@ -139,6 +149,8 @@ func (tc *TracedConn) ExecContext(ctx context.Context, query string, args []driv
default:
}
cquery, spanID := tc.injectComments(ctx, query, tc.cfg.dbmPropagationMode)
ctx, end := startTraceTask(ctx, QueryTypeExec)
defer end()
r, err = execer.Exec(cquery, dargs)
tc.tryTrace(ctx, QueryTypeExec, query, start, err, append(withDBMTraceInjectedTag(tc.cfg.dbmPropagationMode), tracer.WithSpanID(spanID))...)
return r, err
@@ -150,6 +162,8 @@ func (tc *TracedConn) ExecContext(ctx context.Context, query string, args []driv
func (tc *TracedConn) Ping(ctx context.Context) (err error) {
start := time.Now()
if pinger, ok := tc.Conn.(driver.Pinger); ok {
ctx, end := startTraceTask(ctx, QueryTypePing)
defer end()
err = pinger.Ping(ctx)
}
tc.tryTrace(ctx, QueryTypePing, "", start, err)
@@ -162,6 +176,8 @@ func (tc *TracedConn) QueryContext(ctx context.Context, query string, args []dri
start := time.Now()
if queryerContext, ok := tc.Conn.(driver.QueryerContext); ok {
cquery, spanID := tc.injectComments(ctx, query, tc.cfg.dbmPropagationMode)
ctx, end := startTraceTask(ctx, QueryTypeQuery)
defer end()
rows, err := queryerContext.QueryContext(ctx, cquery, args)
tc.tryTrace(ctx, QueryTypeQuery, query, start, err, append(withDBMTraceInjectedTag(tc.cfg.dbmPropagationMode), tracer.WithSpanID(spanID))...)
return rows, err
@@ -177,6 +193,8 @@ func (tc *TracedConn) QueryContext(ctx context.Context, query string, args []dri
default:
}
cquery, spanID := tc.injectComments(ctx, query, tc.cfg.dbmPropagationMode)
ctx, end := startTraceTask(ctx, QueryTypeQuery)
defer end()
rows, err = queryer.Query(cquery, dargs)
tc.tryTrace(ctx, QueryTypeQuery, query, start, err, append(withDBMTraceInjectedTag(tc.cfg.dbmPropagationMode), tracer.WithSpanID(spanID))...)
return rows, err
161 changes: 161 additions & 0 deletions contrib/database/sql/exec_trace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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 2023 Datadog, Inc.

// TODO: gotraceui does not currently handle Go 1.21 execution tracer changes,
// so we need to skip this test for that version. We still have coverage for
// older Go versions due to our support policy, and Go 1.21 shouldn't fundamentally
// change the behavior this test is covering. Remove this build constraint
// once gotraceui supports Go 1.21
//
//go:build !go1.21

package sql

import (
"bytes"
"context"
"io"
"net/http"
"runtime/trace"
"testing"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql/internal"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/httpmem"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gotraceui "honnef.co/go/gotraceui/trace"
)

func TestExecutionTraceAnnotations(t *testing.T) {
if trace.IsEnabled() {
t.Skip("execution tracing is already enabled")
}

// In-memory server & client which discards everything, to avoid
// slowness from unnecessary network I/O
s, c := httpmem.ServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer s.Close()
tracer.Start(tracer.WithHTTPClient(c), tracer.WithLogStartup(false))
defer tracer.Stop()

buf := new(bytes.Buffer)
require.NoError(t, trace.Start(buf), "starting execution tracing")

// sleepDuration is the amount of time our mock DB operations should
// take. We are going to assert that the execution trace tasks
// corresponding with the duration are at least as long as this. In
// reality they could be longer than this due to slow CI, scheduling
// jitter, etc., but we know that they should be at least this long.
const sleepDuration = 10 * time.Millisecond

Register("mock", &internal.MockDriver{
Hook: func() {
time.Sleep(sleepDuration)
},
})
db, err := Open("mock", "")
require.NoError(t, err, "opening mock db")

span, ctx := tracer.StartSpanFromContext(context.Background(), "parent")
_, err = db.ExecContext(ctx, "foobar")
require.NoError(t, err, "executing mock statement")
conn, err := db.Conn(ctx)
require.NoError(t, err, "connecting to DB")
rows, err := conn.QueryContext(ctx, "foobar")
require.NoError(t, err, "executing mock query")
rows.Close()
stmt, err := conn.PrepareContext(ctx, "prepared")
require.NoError(t, err, "preparing mock statement")
_, err = stmt.Exec()
require.NoError(t, err, "executing mock perpared statement")
rows, err = stmt.Query()
require.NoError(t, err, "executing mock perpared query")
rows.Close()
tx, err := conn.BeginTx(ctx, nil)
require.NoError(t, err, "beginning mock transaction")
_, err = tx.ExecContext(ctx, "foobar")
require.NoError(t, err, "executing query in mock transaction")
require.NoError(t, tx.Commit(), "commiting mock transaction")
require.NoError(t, conn.Close())

span.Finish()

trace.Stop()

tasks, err := tasksFromTrace(buf)
require.NoError(t, err, "getting tasks from trace")

expectedParentChildTasks := []string{"Connect", "Exec", "Query", "Prepare", "Begin", "Exec"}
expectedPreparedStatementTasks := []string{"Exec", "Query"}

var foundParent, foundPrepared bool
for id, task := range tasks {
t.Logf("task %d: %+v", id, task)
switch task.name {
case "parent":
foundParent = true
var gotParentChildTasks []string
for _, id := range tasks[id].children {
gotParentChildTasks = append(gotParentChildTasks, tasks[id].name)
}
assert.ElementsMatch(t, expectedParentChildTasks, gotParentChildTasks)
case "Prepare":
foundPrepared = true
var gotPerparedStatementTasks []string
for _, id := range tasks[id].children {
gotPerparedStatementTasks = append(gotPerparedStatementTasks, tasks[id].name)
}
assert.ElementsMatch(t, expectedPreparedStatementTasks, gotPerparedStatementTasks)
assert.GreaterOrEqual(t, task.duration, sleepDuration, "task %s", task.name)
case "Connect", "Exec", "Begin", "Commit", "Query":
assert.GreaterOrEqual(t, task.duration, sleepDuration, "task %s", task.name)
default:
continue
}
}
assert.True(t, foundParent, "need parent task")
assert.True(t, foundPrepared, "need prepared statement task")
}

type traceTask struct {
name string
duration time.Duration
parent int
children []int
}

func tasksFromTrace(r io.Reader) (map[int]traceTask, error) {
execTrace, err := gotraceui.Parse(r, nil)
if err != nil {
return nil, err
}

tasks := make(map[int]traceTask)
for _, ev := range execTrace.Events {
switch ev.Type {
case gotraceui.EvUserTaskCreate:
if ev.Link == -1 {
continue
}
id := int(ev.Args[0])
parent := int(ev.Args[1])
if parent != 0 {
t := tasks[parent]
t.children = append(t.children, id)
tasks[parent] = t
}
name := execTrace.Strings[ev.Args[2]]
tasks[id] = traceTask{
name: name,
parent: parent,
duration: time.Duration(execTrace.Events[ev.Link].Ts - ev.Ts),
}
}
}
return tasks, nil
}
26 changes: 26 additions & 0 deletions contrib/database/sql/internal/mockdriver.go
Original file line number Diff line number Diff line change
@@ -15,10 +15,15 @@ import (
type MockDriver struct {
Prepared []string
Executed []string
// Hook is an optional function to run during a DB operation
Hook func()
}

// Open implements the Conn interface
func (d *MockDriver) Open(_ string) (driver.Conn, error) {
if d.Hook != nil {
d.Hook()
}
return &mockConn{driver: d}, nil
}

@@ -29,18 +34,27 @@ type mockConn struct {
// Prepare implements the driver.Conn interface
func (m *mockConn) Prepare(query string) (driver.Stmt, error) {
m.driver.Prepared = append(m.driver.Prepared, query)
if m.driver.Hook != nil {
m.driver.Hook()
}
return &mockStmt{stmt: query, driver: m.driver}, nil
}

// QueryContext implements the QueryerContext interface
func (m *mockConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
m.driver.Executed = append(m.driver.Executed, query)
if m.driver.Hook != nil {
m.driver.Hook()
}
return &rows{}, nil
}

// ExecContext implements the ExecerContext interface
func (m *mockConn) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) {
m.driver.Executed = append(m.driver.Executed, query)
if m.driver.Hook != nil {
m.driver.Hook()
}
return &mockResult{}, nil
}

@@ -51,6 +65,9 @@ func (m *mockConn) Close() (err error) {

// Begin implements the Conn interface
func (m *mockConn) Begin() (driver.Tx, error) {
if m.driver.Hook != nil {
m.driver.Hook()
}
return &mockTx{driver: m.driver}, nil
}

@@ -77,6 +94,9 @@ type mockTx struct {

// Commit implements the Tx interface
func (t *mockTx) Commit() error {
if t.driver.Hook != nil {
t.driver.Hook()
}
return nil
}

@@ -115,12 +135,18 @@ func (s *mockStmt) Query(_ []driver.Value) (driver.Rows, error) {
// ExecContext implements the StmtExecContext interface
func (s *mockStmt) ExecContext(_ context.Context, _ []driver.NamedValue) (driver.Result, error) {
s.driver.Executed = append(s.driver.Executed, s.stmt)
if s.driver.Hook != nil {
s.driver.Hook()
}
return &mockResult{}, nil
}

// QueryContext implements the StmtQueryContext interface
func (s *mockStmt) QueryContext(_ context.Context, _ []driver.NamedValue) (driver.Rows, error) {
s.driver.Executed = append(s.driver.Executed, s.stmt)
if s.driver.Hook != nil {
s.driver.Hook()
}
return &rows{}, nil
}

2 changes: 2 additions & 0 deletions contrib/database/sql/sql.go
Original file line number Diff line number Diff line change
@@ -152,6 +152,8 @@ func (t *tracedConnector) Connect(ctx context.Context) (driver.Conn, error) {
tp.meta, _ = internal.ParseDSN(t.driverName, t.cfg.dsn)
}
start := time.Now()
ctx, end := startTraceTask(ctx, string(QueryTypeConnect))
defer end()
conn, err := t.connector.Connect(ctx)
tp.tryTrace(ctx, QueryTypeConnect, "", start, err)
if err != nil {
12 changes: 11 additions & 1 deletion contrib/database/sql/stmt.go
Original file line number Diff line number Diff line change
@@ -25,15 +25,19 @@ type tracedStmt struct {
// Close sends a span before closing a statement
func (s *tracedStmt) Close() (err error) {
start := time.Now()
ctx, end := startTraceTask(s.ctx, QueryTypeClose)
defer end()
err = s.Stmt.Close()
s.tryTrace(s.ctx, QueryTypeClose, "", start, err)
s.tryTrace(ctx, QueryTypeClose, "", start, err)
return err
}

// ExecContext is needed to implement the driver.StmtExecContext interface
func (s *tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
start := time.Now()
if stmtExecContext, ok := s.Stmt.(driver.StmtExecContext); ok {
ctx, end := startTraceTask(s.ctx, QueryTypeExec)
defer end()
res, err := stmtExecContext.ExecContext(ctx, args)
s.tryTrace(ctx, QueryTypeExec, s.query, start, err)
return res, err
@@ -47,6 +51,8 @@ func (s *tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue)
return nil, ctx.Err()
default:
}
ctx, end := startTraceTask(s.ctx, QueryTypeExec)
defer end()
res, err = s.Exec(dargs)
s.tryTrace(ctx, QueryTypeExec, s.query, start, err)
return res, err
@@ -56,6 +62,8 @@ func (s *tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue)
func (s *tracedStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
start := time.Now()
if stmtQueryContext, ok := s.Stmt.(driver.StmtQueryContext); ok {
ctx, end := startTraceTask(s.ctx, QueryTypeQuery)
defer end()
rows, err := stmtQueryContext.QueryContext(ctx, args)
s.tryTrace(ctx, QueryTypeQuery, s.query, start, err)
return rows, err
@@ -69,6 +77,8 @@ func (s *tracedStmt) QueryContext(ctx context.Context, args []driver.NamedValue)
return nil, ctx.Err()
default:
}
ctx, end := startTraceTask(s.ctx, QueryTypeQuery)
defer end()
rows, err = s.Query(dargs)
s.tryTrace(ctx, QueryTypeQuery, s.query, start, err)
return rows, err
31 changes: 29 additions & 2 deletions contrib/database/sql/tx.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,10 @@ package sql
import (
"context"
"database/sql/driver"
"runtime/trace"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/internal"
)

var _ driver.Tx = (*tracedTx)(nil)
@@ -20,18 +23,42 @@ type tracedTx struct {
ctx context.Context
}

func noopTaskEnd() {}

// startTraceTask creates an execution trace task with the given name, and
// returns a context.Context associated with the task, and a function to end the
// task.
//
// This is intended for cases where a span would normally be created after an
// operation, where the operation may have been skipped and a span would be
// noisy. Execution trace tasks must cover the actual duration of the operation
// and can't be altered after the fact.
func startTraceTask(ctx context.Context, name string) (context.Context, func()) {
if !trace.IsEnabled() {
return ctx, noopTaskEnd
}
ctx, task := trace.NewTask(ctx, name)
return internal.WithExecutionTraced(ctx), task.End
}

// Commit sends a span at the end of the transaction
func (t *tracedTx) Commit() (err error) {
ctx, end := startTraceTask(t.ctx, QueryTypeCommit)
defer end()

start := time.Now()
err = t.Tx.Commit()
t.tryTrace(t.ctx, QueryTypeCommit, "", start, err)
t.tryTrace(ctx, QueryTypeCommit, "", start, err)
return err
}

// Rollback sends a span if the connection is aborted
func (t *tracedTx) Rollback() (err error) {
ctx, end := startTraceTask(t.ctx, QueryTypeRollback)
defer end()

start := time.Now()
err = t.Tx.Rollback()
t.tryTrace(t.ctx, QueryTypeRollback, "", start, err)
t.tryTrace(ctx, QueryTypeRollback, "", start, err)
return err
}
56 changes: 56 additions & 0 deletions contrib/emicklei/go-restful.v3/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 restful_test

import (
"io"
"log"
"net/http"

restfultrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/emicklei/go-restful.v3"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

"github.com/emicklei/go-restful/v3"
)

// To start tracing requests, add the trace filter to your go-restful router.
func Example() {
// create new go-restful service
ws := new(restful.WebService)

// create the Datadog filter
filter := restfultrace.FilterFunc(
restfultrace.WithServiceName("my-service"),
)

// use it
ws.Filter(filter)

// set endpoint
ws.Route(ws.GET("/hello").To(
func(request *restful.Request, response *restful.Response) {
io.WriteString(response, "world")
}))
restful.Add(ws)

// serve request
log.Fatal(http.ListenAndServe(":8080", nil))
}

func Example_spanFromContext() {
ws := new(restful.WebService)
ws.Filter(restfultrace.FilterFunc(
restfultrace.WithServiceName("my-service"),
))

ws.Route(ws.GET("/image/encode").To(
func(request *restful.Request, response *restful.Response) {
// create a child span to track operation timing.
encodeSpan, _ := tracer.StartSpanFromContext(request.Request.Context(), "image.encode")
// encode a image
encodeSpan.Finish()
}))
}
83 changes: 83 additions & 0 deletions contrib/emicklei/go-restful.v3/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 restful

import (
"math"

"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"
)

const defaultServiceName = "go-restful"

type config struct {
serviceName string
analyticsRate float64
headerTags *internal.LockMap
}

func newConfig() *config {
rate := globalconfig.AnalyticsRate()
if internal.BoolEnv("DD_TRACE_RESTFUL_ANALYTICS_ENABLED", false) {
rate = 1.0
}
serviceName := namingschema.NewDefaultServiceName(
defaultServiceName,
namingschema.WithOverrideV0(defaultServiceName),
).GetName()
return &config{
serviceName: serviceName,
analyticsRate: rate,
headerTags: globalconfig.HeaderTagMap(),
}
}

// Option specifies instrumentation configuration options.
type Option func(*config)

// WithServiceName sets the service name to by used by the filter.
func WithServiceName(name string) Option {
return func(cfg *config) {
cfg.serviceName = name
}
}

// WithAnalytics enables Trace Analytics for all started spans.
func WithAnalytics(on bool) Option {
return func(cfg *config) {
if on {
cfg.analyticsRate = 1.0
} else {
cfg.analyticsRate = math.NaN()
}
}
}

// WithAnalyticsRate sets the sampling rate for Trace Analytics events
// correlated to started spans.
func WithAnalyticsRate(rate float64) Option {
return func(cfg *config) {
if rate >= 0.0 && rate <= 1.0 {
cfg.analyticsRate = rate
} else {
cfg.analyticsRate = math.NaN()
}
}
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
// Special headers can not be sub-selected. E.g., an entire Cookie header would be transmitted, without the ability to choose specific Cookies.
func WithHeaderTags(headers []string) Option {
headerTagsMap := normalizer.HeaderTagSlice(headers)
return func(cfg *config) {
cfg.headerTags = internal.NewLockMap(headerTagsMap)
}
}
54 changes: 54 additions & 0 deletions contrib/emicklei/go-restful.v3/restful.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 restful provides functions to trace the emicklei/go-restful package (https://github.com/emicklei/go-restful).
package restful

import (
"math"

"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"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/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry"

"github.com/emicklei/go-restful/v3"
)

const componentName = "emicklei/go-restful.v3"

func init() {
telemetry.LoadIntegration(componentName)
}

// FilterFunc returns a restful.FilterFunction which will automatically trace incoming request.
func FilterFunc(configOpts ...Option) restful.FilterFunction {
cfg := newConfig()
for _, opt := range configOpts {
opt(cfg)
}
log.Debug("contrib/emicklei/go-restful/v3: Creating tracing filter: %#v", cfg)
spanOpts := []ddtrace.StartSpanOption{tracer.ServiceName(cfg.serviceName)}
return func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
spanOpts := append(spanOpts, tracer.ResourceName(req.SelectedRoutePath()))
spanOpts = append(spanOpts, tracer.Tag(ext.Component, componentName))
spanOpts = append(spanOpts, tracer.Tag(ext.SpanKind, ext.SpanKindServer))

if !math.IsNaN(cfg.analyticsRate) {
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...)
defer func() {
httptrace.FinishRequestSpan(span, resp.StatusCode(), tracer.WithError(resp.Error()))
}()

// pass the span through the request context
req.Request = req.Request.WithContext(ctx)
chain.ProcessFilter(req, resp)
}
}
324 changes: 324 additions & 0 deletions contrib/emicklei/go-restful.v3/restful_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
// 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 restful

import (
"errors"
"math"
"net/http"
"net/http/httptest"
"strings"
"testing"

"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest"
"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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/emicklei/go-restful/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestWithHeaderTags(t *testing.T) {
setupReq := func(opts ...Option) *http.Request {
ws := new(restful.WebService)
ws.Filter(FilterFunc(opts...))
ws.Route(ws.GET("/test").To(func(request *restful.Request, response *restful.Response) {
response.Write([]byte("test"))
}))

container := restful.NewContainer()
container.Add(ws)

r := httptest.NewRequest("GET", "/test", nil)
r.Header.Set("h!e@a-d.e*r", "val")
r.Header.Add("h!e@a-d.e*r", "val2")
r.Header.Set("2header", "2val")
r.Header.Set("3header", "3val")
r.Header.Set("x-datadog-header", "value")
w := httptest.NewRecorder()

container.ServeHTTP(w, r)
return r
}

t.Run("default-off", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header", "3header", "x-datadog-header"}
setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
_, tag := normalizer.HeaderTag(arg)
assert.NotContains(s.Tags(), tag)
}
})

t.Run("integration", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

header, tag := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(header, tag)
globalconfig.SetHeaderTag("other", tag)

r := setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

globalH, globalT := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(globalH, globalT)

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
assert.NotContains(s.Tags(), globalT)
})
}

func TestTrace200(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

ws := new(restful.WebService)
ws.Filter(FilterFunc(WithServiceName("my-service")))
ws.Route(ws.GET("/user/{id}").Param(restful.PathParameter("id", "user ID")).
To(func(request *restful.Request, response *restful.Response) {
_, ok := tracer.SpanFromContext(request.Request.Context())
assert.True(ok)
id := request.PathParameter("id")
response.Write([]byte(id))
}))

container := restful.NewContainer()
container.Add(ws)

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

container.ServeHTTP(w, r)
response := w.Result()
defer response.Body.Close()
assert.Equal(response.StatusCode, 200)

spans := mt.FinishedSpans()
assert.Len(spans, 1)
span := spans[0]
assert.Equal("http.request", span.OperationName())
assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType))
assert.Contains(span.Tag(ext.ResourceName), "/user/{id}")
assert.Equal("my-service", span.Tag(ext.ServiceName))
assert.Equal("200", span.Tag(ext.HTTPCode))
assert.Equal("GET", span.Tag(ext.HTTPMethod))
assert.Equal("http://example.com/user/123", span.Tag(ext.HTTPURL))
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
assert.Equal("emicklei/go-restful.v3", span.Tag(ext.Component))
}

func TestError(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

wantErr := errors.New("oh no")

ws := new(restful.WebService)
ws.Filter(FilterFunc())
ws.Route(ws.GET("/err").To(func(request *restful.Request, response *restful.Response) {
response.WriteError(500, wantErr)
}))

container := restful.NewContainer()
container.Add(ws)

r := httptest.NewRequest("GET", "/err", nil)
w := httptest.NewRecorder()

container.ServeHTTP(w, r)
response := w.Result()
defer response.Body.Close()
assert.Equal(response.StatusCode, 500)

spans := mt.FinishedSpans()
assert.Len(spans, 1)
span := spans[0]
assert.Equal("http.request", span.OperationName())
assert.Equal("500", span.Tag(ext.HTTPCode))
assert.Equal(wantErr.Error(), span.Tag(ext.Error).(error).Error())
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
assert.Equal("emicklei/go-restful.v3", span.Tag(ext.Component))
}

func TestPropagation(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

pspan := tracer.StartSpan("test")
tracer.Inject(pspan.Context(), tracer.HTTPHeadersCarrier(r.Header))

ws := new(restful.WebService)
ws.Filter(FilterFunc())
ws.Route(ws.GET("/user/{id}").To(func(request *restful.Request, response *restful.Response) {
span, ok := tracer.SpanFromContext(request.Request.Context())
assert.True(ok)
assert.Equal(span.(mocktracer.Span).ParentID(), pspan.(mocktracer.Span).SpanID())
}))

container := restful.NewContainer()
container.Add(ws)

container.ServeHTTP(w, r)
}

func TestAnalyticsSettings(t *testing.T) {
assertRate := func(t *testing.T, mt mocktracer.Tracer, rate float64, opts ...Option) {
ws := new(restful.WebService)
ws.Filter(FilterFunc(opts...))
ws.Route(ws.GET("/user/{id}").To(func(request *restful.Request, response *restful.Response) {}))

container := restful.NewContainer()
container.Add(ws)
r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
container.ServeHTTP(w, r)

spans := mt.FinishedSpans()
assert.Len(t, spans, 1)
s := spans[0]
if !math.IsNaN(rate) {
assert.Equal(t, rate, s.Tag(ext.EventSampleRate))
}
}

t.Run("defaults", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, globalconfig.AnalyticsRate())
})

t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

rate := globalconfig.AnalyticsRate()
defer globalconfig.SetAnalyticsRate(rate)
globalconfig.SetAnalyticsRate(0.4)

assertRate(t, mt, 0.4)
})

t.Run("enabled", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, 1.0, WithAnalytics(true))
})

t.Run("disabled", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, math.NaN(), WithAnalytics(false))
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

rate := globalconfig.AnalyticsRate()
defer globalconfig.SetAnalyticsRate(rate)
globalconfig.SetAnalyticsRate(0.4)

assertRate(t, mt, 0.23, WithAnalyticsRate(0.23))
})
}

func TestNamingSchema(t *testing.T) {
genSpans := namingschematest.GenSpansFn(func(t *testing.T, serviceOverride string) []mocktracer.Span {
var opts []Option
if serviceOverride != "" {
opts = append(opts, WithServiceName(serviceOverride))
}
mt := mocktracer.Start()
defer mt.Stop()

ws := new(restful.WebService)
ws.Filter(FilterFunc(opts...))
ws.Route(ws.GET("/user/{id}").Param(restful.PathParameter("id", "user ID")).
To(func(request *restful.Request, response *restful.Response) {
_, err := response.Write([]byte(request.PathParameter("id")))
require.NoError(t, err)
}))
container := restful.NewContainer()
container.Add(ws)

r := httptest.NewRequest("GET", "/user/200", nil)
w := httptest.NewRecorder()
container.ServeHTTP(w, r)

return mt.FinishedSpans()
})
wantServiceNameV0 := namingschematest.ServiceNameAssertions{
WithDefaults: []string{"go-restful"},
WithDDService: []string{"go-restful"},
WithDDServiceAndOverride: []string{namingschematest.TestServiceOverride},
}
namingschematest.NewHTTPServerTest(
genSpans,
"go-restful",
namingschematest.WithServiceNameAssertions(namingschema.SchemaV0, wantServiceNameV0),
)(t)
}
14 changes: 14 additions & 0 deletions contrib/emicklei/go-restful/option.go
Original file line number Diff line number Diff line change
@@ -11,13 +11,15 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"
)

const defaultServiceName = "go-restful"

type config struct {
serviceName string
analyticsRate float64
headerTags *internal.LockMap
}

func newConfig() *config {
@@ -32,6 +34,7 @@ func newConfig() *config {
return &config{
serviceName: serviceName,
analyticsRate: rate,
headerTags: globalconfig.HeaderTagMap(),
}
}

@@ -67,3 +70,14 @@ func WithAnalyticsRate(rate float64) Option {
}
}
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
// Special headers can not be sub-selected. E.g., an entire Cookie header would be transmitted, without the ability to choose specific Cookies.
func WithHeaderTags(headers []string) Option {
headerTagsMap := normalizer.HeaderTagSlice(headers)
return func(cfg *config) {
cfg.headerTags = internal.NewLockMap(headerTagsMap)
}
}
1 change: 1 addition & 0 deletions contrib/emicklei/go-restful/restful.go
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ func FilterFunc(configOpts ...Option) restful.FilterFunction {
if !math.IsNaN(cfg.analyticsRate) {
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...)
defer func() {
httptrace.FinishRequestSpan(span, resp.StatusCode(), tracer.WithError(resp.Error()))
100 changes: 100 additions & 0 deletions contrib/emicklei/go-restful/restful_test.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@ package restful
import (
"errors"
"math"
"net/http"
"net/http/httptest"
"strings"
"testing"

"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest"
@@ -17,12 +19,110 @@ import (
"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/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/emicklei/go-restful"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestWithHeaderTags(t *testing.T) {
setupReq := func(opts ...Option) *http.Request {
ws := new(restful.WebService)
ws.Filter(FilterFunc(opts...))
ws.Route(ws.GET("/test").To(func(request *restful.Request, response *restful.Response) {
response.Write([]byte("test"))
}))

container := restful.NewContainer()
container.Add(ws)

r := httptest.NewRequest("GET", "/test", nil)
r.Header.Set("h!e@a-d.e*r", "val")
r.Header.Add("h!e@a-d.e*r", "val2")
r.Header.Set("2header", "2val")
r.Header.Set("3header", "3val")
r.Header.Set("x-datadog-header", "value")
w := httptest.NewRecorder()

container.ServeHTTP(w, r)
return r
}

t.Run("default-off", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header", "3header", "x-datadog-header"}
setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
_, tag := normalizer.HeaderTag(arg)
assert.NotContains(s.Tags(), tag)
}
})

t.Run("integration", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

header, tag := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(header, tag)
globalconfig.SetHeaderTag("other", tag)

r := setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

globalH, globalT := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(globalH, globalT)

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
assert.NotContains(s.Tags(), globalT)
})
}

func TestTrace200(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
2 changes: 1 addition & 1 deletion contrib/gin-gonic/gin/gintrace.go
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc {
opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
}
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...)
defer func() {
httptrace.FinishRequestSpan(span, c.Writer.Status())
89 changes: 89 additions & 0 deletions contrib/gin-gonic/gin/gintrace_test.go
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
@@ -18,6 +19,7 @@ import (
"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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
@@ -437,6 +439,93 @@ func TestResourceNamerSettings(t *testing.T) {
})
}

func TestWithHeaderTags(t *testing.T) {
setupReq := func(opts ...Option) *http.Request {
router := gin.New()
router.Use(Middleware("gin", opts...))

router.GET("/test", func(c *gin.Context) {
c.Writer.Write([]byte("test"))
})
r := httptest.NewRequest("GET", "/test", nil)
r.Header.Set("h!e@a-d.e*r", "val")
r.Header.Add("h!e@a-d.e*r", "val2")
r.Header.Set("2header", "2val")
r.Header.Set("3header", "3val")
r.Header.Set("x-datadog-header", "value")
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
return r
}
t.Run("default-off", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header", "3header", "x-datadog-header"}
setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
_, tag := normalizer.HeaderTag(arg)
assert.NotContains(s.Tags(), tag)
}
})
t.Run("integration", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})
t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

header, tag := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(header, tag)

r := setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

globalH, globalT := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(globalH, globalT)

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
assert.NotContains(s.Tags(), globalT)
})
}

func TestIgnoreRequestSettings(t *testing.T) {
router := gin.New()
router.Use(Middleware("foobar", WithIgnoreRequest(func(c *gin.Context) bool {
14 changes: 14 additions & 0 deletions contrib/gin-gonic/gin/option.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/gin-gonic/gin"
)
@@ -23,6 +24,7 @@ type config struct {
resourceNamer func(c *gin.Context) string
serviceName string
ignoreRequest func(c *gin.Context) bool
headerTags *internal.LockMap
}

func newConfig(serviceName string) *config {
@@ -38,6 +40,7 @@ func newConfig(serviceName string) *config {
resourceNamer: defaultResourceNamer,
serviceName: serviceName,
ignoreRequest: func(_ *gin.Context) bool { return false },
headerTags: globalconfig.HeaderTagMap(),
}
}

@@ -75,6 +78,17 @@ func WithResourceNamer(namer func(c *gin.Context) string) Option {
}
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
// Special headers can not be sub-selected. E.g., an entire Cookie header would be transmitted, without the ability to choose specific Cookies.
func WithHeaderTags(headers []string) Option {
headerTagsMap := normalizer.HeaderTagSlice(headers)
return func(cfg *config) {
cfg.headerTags = internal.NewLockMap(headerTagsMap)
}
}

// WithIgnoreRequest specifies a function to use for determining if the
// incoming HTTP request tracing should be skipped.
func WithIgnoreRequest(f func(c *gin.Context) bool) Option {
1 change: 1 addition & 0 deletions contrib/go-chi/chi.v5/chi.go
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler {
if !math.IsNaN(cfg.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
}
opts = append(opts, httptrace.HeaderTagsFromRequest(r, cfg.headerTags))
span, ctx := httptrace.StartRequestSpan(r, opts...)
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
92 changes: 92 additions & 0 deletions contrib/go-chi/chi.v5/chi_test.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
@@ -449,6 +450,97 @@ func TestAppSec(t *testing.T) {
})
}

func TestWithHeaderTags(t *testing.T) {
setupReq := func(opts ...Option) *http.Request {
router := chi.NewRouter()
router.Use(Middleware(opts...))

router.Get("/test", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("test"))
})
r := httptest.NewRequest("GET", "/test", nil)
r.Header.Set("h!e@a-d.e*r", "val")
r.Header.Add("h!e@a-d.e*r", "val2")
r.Header.Set("2header", "2val")
r.Header.Set("3header", "3val")
r.Header.Set("x-datadog-header", "value")
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
return r
}

t.Run("default-off", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header", "3header", "x-datadog-header"}
setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
_, tag := normalizer.HeaderTag(arg)
assert.NotContains(s.Tags(), tag)
}
})

t.Run("integration", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

header, tag := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(header, tag)

r := setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

globalH, globalT := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(globalH, globalT)

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
assert.NotContains(s.Tags(), globalT)
})
}
func TestNamingSchema(t *testing.T) {
genSpans := namingschematest.GenSpansFn(func(t *testing.T, serviceOverride string) []mocktracer.Span {
var opts []Option
14 changes: 14 additions & 0 deletions contrib/go-chi/chi.v5/option.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"
)

const defaultServiceName = "chi.router"
@@ -24,6 +25,7 @@ type config struct {
isStatusError func(statusCode int) bool
ignoreRequest func(r *http.Request) bool
modifyResourceName func(resourceName string) string
headerTags *internal.LockMap
resourceNamer func(r *http.Request) string
}

@@ -37,6 +39,7 @@ func defaults(cfg *config) {
} else {
cfg.analyticsRate = globalconfig.AnalyticsRate()
}
cfg.headerTags = globalconfig.HeaderTagMap()
cfg.isStatusError = isServerError
cfg.ignoreRequest = func(_ *http.Request) bool { return false }
cfg.modifyResourceName = func(s string) string { return s }
@@ -109,6 +112,17 @@ func WithModifyResourceName(fn func(resourceName string) string) Option {
}
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
// Special headers can not be sub-selected. E.g., an entire Cookie header would be transmitted, without the ability to choose specific Cookies.
func WithHeaderTags(headers []string) Option {
headerTagsMap := normalizer.HeaderTagSlice(headers)
return func(cfg *config) {
cfg.headerTags = internal.NewLockMap(headerTagsMap)
}
}

// WithResourceNamer specifies a function to use for determining the resource
// name of the span.
func WithResourceNamer(fn func(r *http.Request) string) Option {
1 change: 1 addition & 0 deletions contrib/go-chi/chi/chi.go
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler {
if !math.IsNaN(cfg.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
}
opts = append(opts, httptrace.HeaderTagsFromRequest(r, cfg.headerTags))
span, ctx := httptrace.StartRequestSpan(r, opts...)
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
93 changes: 93 additions & 0 deletions contrib/go-chi/chi/chi_test.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
@@ -181,6 +182,98 @@ func TestError(t *testing.T) {
})
}

func TestWithHeaderTags(t *testing.T) {
setupReq := func(opts ...Option) *http.Request {
router := chi.NewRouter()
router.Use(Middleware(opts...))

router.Get("/test", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("test"))
})
r := httptest.NewRequest("GET", "/test", nil)
r.Header.Set("h!e@a-d.e*r", "val")
r.Header.Add("h!e@a-d.e*r", "val2")
r.Header.Set("2header", "2val")
r.Header.Set("3header", "3val")
r.Header.Set("x-datadog-header", "value")
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
return r
}

t.Run("default-off", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
htArgs := []string{"h!e@a-d.e*r", "2header", "3header", "x-datadog-header"}
setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]
for _, arg := range htArgs {
_, tag := normalizer.HeaderTag(arg)
assert.NotContains(s.Tags(), tag)
}
})

t.Run("integration", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("global", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

header, tag := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(header, tag)

r := setupReq()
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

globalH, globalT := normalizer.HeaderTag("3header")
globalconfig.SetHeaderTag(globalH, globalT)

htArgs := []string{"h!e@a-d.e*r", "2header:tag"}
r := setupReq(WithHeaderTags(htArgs))
spans := mt.FinishedSpans()
assert := assert.New(t)
assert.Equal(len(spans), 1)
s := spans[0]

for _, arg := range htArgs {
header, tag := normalizer.HeaderTag(arg)
assert.Equal(strings.Join(r.Header.Values(header), ","), s.Tags()[tag])
}
assert.NotContains(s.Tags(), "http.headers.x-datadog-header")
assert.NotContains(s.Tags(), globalT)
})
}

func TestGetSpanNotInstrumented(t *testing.T) {
assert := assert.New(t)
router := chi.NewRouter()
14 changes: 14 additions & 0 deletions contrib/go-chi/chi/option.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/go-chi/chi"
)
@@ -26,6 +27,7 @@ type config struct {
isStatusError func(statusCode int) bool
ignoreRequest func(r *http.Request) bool
resourceNamer func(r *http.Request) string
headerTags *internal.LockMap
}

// Option represents an option that can be passed to NewRouter.
@@ -38,6 +40,7 @@ func defaults(cfg *config) {
} else {
cfg.analyticsRate = globalconfig.AnalyticsRate()
}
cfg.headerTags = globalconfig.HeaderTagMap()
cfg.isStatusError = isServerError
cfg.ignoreRequest = func(_ *http.Request) bool { return false }
cfg.resourceNamer = func(r *http.Request) string {
@@ -100,6 +103,17 @@ func isServerError(statusCode int) bool {
return statusCode >= 500 && statusCode < 600
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
// Special headers can not be sub-selected. E.g., an entire Cookie header would be transmitted, without the ability to choose specific Cookies.
func WithHeaderTags(headers []string) Option {
headerTagsMap := normalizer.HeaderTagSlice(headers)
return func(cfg *config) {
cfg.headerTags = internal.NewLockMap(headerTagsMap)
}
}

// WithIgnoreRequest specifies a function to use for determining if the
// incoming HTTP request tracing should be skipped.
func WithIgnoreRequest(fn func(r *http.Request) bool) Option {
10 changes: 10 additions & 0 deletions contrib/go-redis/redis.v7/option.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ type clientConfig struct {
serviceName string
spanName string
analyticsRate float64
errCheck func(error) bool
}

// ClientOption represents an option that can be used to create or wrap a client.
@@ -35,6 +36,7 @@ func defaults(cfg *clientConfig) {
} else {
cfg.analyticsRate = math.NaN()
}
cfg.errCheck = func(error) bool { return true }
}

// WithServiceName sets the given service name for the client.
@@ -66,3 +68,11 @@ func WithAnalyticsRate(rate float64) ClientOption {
}
}
}

// WithErrorCheck specifies a function fn which determines whether the passed
// error should be marked as an error.
func WithErrorCheck(fn func(err error) bool) ClientOption {
return func(cfg *clientConfig) {
cfg.errCheck = fn
}
}
4 changes: 2 additions & 2 deletions contrib/go-redis/redis.v7/redis.go
Original file line number Diff line number Diff line change
@@ -134,7 +134,7 @@ func (ddh *datadogHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error
span, _ = tracer.SpanFromContext(ctx)
var finishOpts []ddtrace.FinishOption
errRedis := cmd.Err()
if errRedis != redis.Nil {
if errRedis != redis.Nil && ddh.config.errCheck(errRedis) {
finishOpts = append(finishOpts, tracer.WithError(errRedis))
}
span.Finish(finishOpts...)
@@ -172,7 +172,7 @@ func (ddh *datadogHook) AfterProcessPipeline(ctx context.Context, cmds []redis.C
var finishOpts []ddtrace.FinishOption
for _, cmd := range cmds {
errCmd := cmd.Err()
if errCmd != redis.Nil {
if errCmd != redis.Nil && ddh.config.errCheck(errCmd) {
finishOpts = append(finishOpts, tracer.WithError(errCmd))
}
}
35 changes: 35 additions & 0 deletions contrib/go-redis/redis.v7/redis_test.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ package redis

import (
"context"
"errors"
"fmt"
"os"
"testing"
@@ -382,6 +383,40 @@ func TestError(t *testing.T) {
assert.Equal("0", span.Tag("out.db"))
assert.Equal(0, span.Tag(ext.RedisDatabaseIndex))
})

t.Run("errcheck", func(t *testing.T) {
opts := &redis.Options{Addr: "127.0.0.1:6379"}
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

errCheckFn := func(err error) bool {
return err != nil && !errors.Is(err, context.Canceled)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()

client := NewClient(opts, WithServiceName("my-redis"), WithErrorCheck(errCheckFn))
client = client.WithContext(ctx)

_, err := client.Get("test_key").Result()

spans := mt.FinishedSpans()
assert.Len(spans, 1)
span := spans[0]

assert.Equal(context.Canceled, err)
assert.Empty(span.Tag(ext.Error))
assert.Equal("redis.command", span.OperationName())
assert.Equal("127.0.0.1", span.Tag(ext.TargetHost))
assert.Equal("6379", span.Tag(ext.TargetPort))
assert.Equal("get test_key: ", span.Tag("redis.raw_command"))
assert.Equal("go-redis/redis.v7", span.Tag(ext.Component))
assert.Equal(ext.SpanKindClient, span.Tag(ext.SpanKind))
assert.Equal("redis", span.Tag(ext.DBSystem))
assert.Equal("0", span.Tag("out.db"))
assert.Equal(0, span.Tag(ext.RedisDatabaseIndex))
})
}
func TestAnalyticsSettings(t *testing.T) {
assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...ClientOption) {
10 changes: 10 additions & 0 deletions contrib/go-redis/redis.v8/option.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ type clientConfig struct {
spanName string
analyticsRate float64
skipRaw bool
errCheck func(err error) bool
}

// ClientOption represents an option that can be used to create or wrap a client.
@@ -36,6 +37,7 @@ func defaults(cfg *clientConfig) {
} else {
cfg.analyticsRate = math.NaN()
}
cfg.errCheck = func(error) bool { return true }
}

// WithSkipRawCommand reports whether to skip setting the "redis.raw_command" tag
@@ -76,3 +78,11 @@ func WithAnalyticsRate(rate float64) ClientOption {
}
}
}

// WithErrorCheck specifies a function fn which determines whether the passed
// error should be marked as an error.
func WithErrorCheck(fn func(err error) bool) ClientOption {
return func(cfg *clientConfig) {
cfg.errCheck = fn
}
}
4 changes: 2 additions & 2 deletions contrib/go-redis/redis.v8/redis.go
Original file line number Diff line number Diff line change
@@ -136,7 +136,7 @@ func (ddh *datadogHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error
span, _ = tracer.SpanFromContext(ctx)
var finishOpts []ddtrace.FinishOption
errRedis := cmd.Err()
if errRedis != redis.Nil {
if errRedis != redis.Nil && ddh.config.errCheck(errRedis) {
finishOpts = append(finishOpts, tracer.WithError(errRedis))
}
span.Finish(finishOpts...)
@@ -176,7 +176,7 @@ func (ddh *datadogHook) AfterProcessPipeline(ctx context.Context, cmds []redis.C
var finishOpts []ddtrace.FinishOption
for _, cmd := range cmds {
errCmd := cmd.Err()
if errCmd != redis.Nil {
if errCmd != redis.Nil && ddh.config.errCheck(errCmd) {
finishOpts = append(finishOpts, tracer.WithError(errCmd))
}
}
33 changes: 33 additions & 0 deletions contrib/go-redis/redis.v8/redis_test.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ package redis

import (
"context"
"errors"
"fmt"
"os"
"testing"
@@ -463,6 +464,38 @@ func TestError(t *testing.T) {
assert.Equal("0", span.Tag("out.db"))
assert.Equal(0, span.Tag(ext.RedisDatabaseIndex))
})

t.Run("errcheck", func(t *testing.T) {
opts := &redis.Options{Addr: "127.0.0.1:6379"}
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

errCheckFn := func(err error) bool {
return err != nil && !errors.Is(err, context.Canceled)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()

client := NewClient(opts, WithServiceName("my-redis"), WithErrorCheck(errCheckFn))
_, err := client.Get(ctx, "test_key").Result()

spans := mt.FinishedSpans()
assert.Len(spans, 1)
span := spans[0]

assert.Equal(context.Canceled, err)
assert.Empty(span.Tag(ext.Error))
assert.Equal("redis.command", span.OperationName())
assert.Equal("127.0.0.1", span.Tag(ext.TargetHost))
assert.Equal("6379", span.Tag(ext.TargetPort))
assert.Equal("get test_key:", span.Tag("redis.raw_command"))
assert.Equal("go-redis/redis.v8", span.Tag(ext.Component))
assert.Equal(ext.SpanKindClient, span.Tag(ext.SpanKind))
assert.Equal("redis", span.Tag(ext.DBSystem))
assert.Equal("0", span.Tag("out.db"))
assert.Equal(0, span.Tag(ext.RedisDatabaseIndex))
})
}
func TestAnalyticsSettings(t *testing.T) {
assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...ClientOption) {
14 changes: 6 additions & 8 deletions contrib/gocql/gocql/example_test.go
Original file line number Diff line number Diff line change
@@ -11,14 +11,11 @@ import (
gocqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gocql/gocql"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

"github.com/gocql/gocql"
)

// To trace Cassandra commands, use our query wrapper WrapQuery.
func Example() {
// Initialise a Cassandra session as usual, create a query.
cluster := gocql.NewCluster("127.0.0.1")
// Initialise a wrapped Cassandra session and create a query.
cluster := gocqltrace.NewCluster([]string{"127.0.0.1"}, gocqltrace.WithServiceName("ServiceName"))
session, _ := cluster.CreateSession()
query := session.Query("CREATE KEYSPACE if not exists trace WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1}")

@@ -30,9 +27,10 @@ func Example() {
)

// Wrap the query to trace it and pass the context for inheritance
tracedQuery := gocqltrace.WrapQuery(query, gocqltrace.WithServiceName("ServiceName"))
tracedQuery.WithContext(ctx)
query.WithContext(ctx)
// Provide any options for the specific query.
query.WithWrapOptions(gocqltrace.WithResourceName("CREATE KEYSPACE"))

// Execute your query as usual
tracedQuery.Exec()
query.Exec()
}
135 changes: 115 additions & 20 deletions contrib/gocql/gocql/gocql.go
Original file line number Diff line number Diff line change
@@ -28,23 +28,53 @@ func init() {
telemetry.LoadIntegration(componentName)
}

// ClusterConfig embeds gocql.ClusterConfig and keeps information relevant to tracing.
type ClusterConfig struct {
*gocql.ClusterConfig
hosts []string
opts []WrapOption
}

// NewCluster calls gocql.NewCluster and returns a wrapped instrumented version of it.
func NewCluster(hosts []string, opts ...WrapOption) *ClusterConfig {
return &ClusterConfig{
ClusterConfig: gocql.NewCluster(hosts...),
hosts: hosts,
opts: opts,
}
}

// Session embeds gocql.Session and keeps information relevant to tracing.
type Session struct {
*gocql.Session
hosts []string
opts []WrapOption
}

// CreateSession calls the underlying gocql.ClusterConfig's CreateSession method and returns a new Session augmented with tracing.
func (c *ClusterConfig) CreateSession() (*Session, error) {
s, err := c.ClusterConfig.CreateSession()
if err != nil {
return nil, err
}
return &Session{
Session: s,
hosts: c.hosts,
opts: c.opts,
}, nil
}

// Query inherits from gocql.Query, it keeps the tracer and the context.
type Query struct {
*gocql.Query
*params
ctx context.Context
}

// Iter inherits from gocql.Iter and contains a span.
type Iter struct {
*gocql.Iter
span ddtrace.Span
}

// Scanner inherits from a gocql.Scanner derived from an Iter
type Scanner struct {
gocql.Scanner
span ddtrace.Span
// Query calls the underlying gocql.Session's Query method and returns a new Query augmented with tracing.
func (s *Session) Query(stmt string, values ...interface{}) *Query {
q := s.Session.Query(stmt, values...)
return wrapQuery(q, s.hosts, s.opts...)
}

// Batch inherits from gocql.Batch, it keeps the tracer and the context.
@@ -54,11 +84,18 @@ type Batch struct {
ctx context.Context
}

// params containes fields and metadata useful for command tracing
// NewBatch calls the underlying gocql.Session's NewBatch method and returns a new Batch augmented with tracing.
func (s *Session) NewBatch(typ gocql.BatchType) *Batch {
b := s.Session.NewBatch(typ)
return wrapBatch(b, s.hosts, s.opts...)
}

// params contains fields and metadata useful for command tracing
type params struct {
config *queryConfig
keyspace string
paginated bool
config *queryConfig
keyspace string
paginated bool
clusterContactPoints string
}

// WrapQuery wraps a gocql.Query into a traced Query under the given service name.
@@ -70,9 +107,14 @@ type params struct {
// To be more specific: it is ok (and recommended) to use and chain the return value
// of `WithContext` and `PageState` but not that of `Consistency`, `Trace`,
// `Observer`, etc.
//
// Deprecated: initialize your ClusterConfig with NewCluster instead.
func WrapQuery(q *gocql.Query, opts ...WrapOption) *Query {
cfg := new(queryConfig)
defaults(cfg)
return wrapQuery(q, nil, opts...)
}

func wrapQuery(q *gocql.Query, hosts []string, opts ...WrapOption) *Query {
cfg := defaultConfig()
for _, fn := range opts {
fn(cfg)
}
@@ -81,8 +123,12 @@ func WrapQuery(q *gocql.Query, opts ...WrapOption) *Query {
cfg.resourceName = parts[1]
}
}
p := &params{config: cfg}
if len(hosts) > 0 {
p.clusterContactPoints = strings.Join(hosts, ",")
}
log.Debug("contrib/gocql/gocql: Wrapping Query: %#v", cfg)
tq := &Query{q, &params{config: cfg}, q.Context()}
tq := &Query{Query: q, params: p, ctx: q.Context()}
return tq
}

@@ -93,6 +139,14 @@ func (tq *Query) WithContext(ctx context.Context) *Query {
return tq
}

// WithWrapOptions applies the given set of options to the query.
func (tq *Query) WithWrapOptions(opts ...WrapOption) *Query {
for _, fn := range opts {
fn(tq.params.config)
}
return tq
}

// PageState rewrites the original function so that spans are aware of the change.
func (tq *Query) PageState(state []byte) *Query {
tq.params.paginated = true
@@ -116,6 +170,12 @@ func (tq *Query) newChildSpan(ctx context.Context) ddtrace.Span {
if !math.IsNaN(p.config.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate))
}
if tq.clusterContactPoints != "" {
opts = append(opts, tracer.Tag(ext.CassandraContactPoints, tq.clusterContactPoints))
}
for k, v := range tq.config.customTags {
opts = append(opts, tracer.Tag(k, v))
}
span, _ := tracer.StartSpanFromContext(ctx, p.config.querySpanName, opts...)
return span
}
@@ -160,6 +220,12 @@ func (tq *Query) ScanCAS(dest ...interface{}) (applied bool, err error) {
return applied, err
}

// Iter inherits from gocql.Iter and contains a span.
type Iter struct {
*gocql.Iter
span ddtrace.Span
}

// Iter starts a new span at query.Iter call.
func (tq *Query) Iter() *Iter {
span := tq.newChildSpan(tq.ctx)
@@ -190,6 +256,12 @@ func (tIter *Iter) Close() error {
return err
}

// Scanner inherits from a gocql.Scanner derived from an Iter
type Scanner struct {
gocql.Scanner
span ddtrace.Span
}

// Scanner returns a row Scanner which provides an interface to scan rows in a
// manner which is similar to database/sql. The Iter should NOT be used again after
// calling this method.
@@ -219,14 +291,23 @@ func (s *Scanner) Err() error {
// To be more specific: it is ok (and recommended) to use and chain the return value
// of `WithContext` and `WithTimestamp` but not that of `SerialConsistency`, `Trace`,
// `Observer`, etc.
//
// Deprecated: initialize your ClusterConfig with NewCluster instead.
func WrapBatch(b *gocql.Batch, opts ...WrapOption) *Batch {
cfg := new(queryConfig)
defaults(cfg)
return wrapBatch(b, nil, opts...)
}

func wrapBatch(b *gocql.Batch, hosts []string, opts ...WrapOption) *Batch {
cfg := defaultConfig()
for _, fn := range opts {
fn(cfg)
}
p := &params{config: cfg}
if len(hosts) > 0 {
p.clusterContactPoints = strings.Join(hosts, ",")
}
log.Debug("contrib/gocql/gocql: Wrapping Batch: %#v", cfg)
tb := &Batch{b, &params{config: cfg}, b.Context()}
tb := &Batch{Batch: b, params: p, ctx: b.Context()}
return tb
}

@@ -237,6 +318,14 @@ func (tb *Batch) WithContext(ctx context.Context) *Batch {
return tb
}

// WithWrapOptions applies the given set of options to the batch.
func (tb *Batch) WithWrapOptions(opts ...WrapOption) *Batch {
for _, fn := range opts {
fn(tb.params.config)
}
return tb
}

// WithTimestamp will enable the with default timestamp flag on the query like
// DefaultTimestamp does. But also allows to define value for timestamp. It works the
// same way as USING TIMESTAMP in the query itself, but should not break prepared
@@ -270,6 +359,12 @@ func (tb *Batch) newChildSpan(ctx context.Context) ddtrace.Span {
if !math.IsNaN(p.config.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate))
}
if tb.clusterContactPoints != "" {
opts = append(opts, tracer.Tag(ext.CassandraContactPoints, tb.clusterContactPoints))
}
for k, v := range tb.config.customTags {
opts = append(opts, tracer.Tag(k, v))
}
span, _ := tracer.StartSpanFromContext(ctx, p.config.batchSpanName, opts...)
return span
}
163 changes: 152 additions & 11 deletions contrib/gocql/gocql/gocql_test.go
Original file line number Diff line number Diff line change
@@ -31,18 +31,28 @@ const (
)

func newCassandraCluster() *gocql.ClusterConfig {
cluster := gocql.NewCluster(cassandraHost)
cfg := gocql.NewCluster(cassandraHost)
updateTestClusterConfig(cfg)
return cfg
}

func newTracedCassandraCluster(opts ...WrapOption) *ClusterConfig {
cfg := NewCluster([]string{cassandraHost}, opts...)
updateTestClusterConfig(cfg.ClusterConfig)
return cfg
}

func updateTestClusterConfig(cfg *gocql.ClusterConfig) {
// the InitialHostLookup must be disabled in newer versions of
// gocql otherwise "no connections were made when creating the session"
// error is returned for Cassandra misconfiguration (that we don't need
// since we're testing another behavior and not the client).
// Check: https://github.com/gocql/gocql/issues/946
cluster.DisableInitialHostLookup = true
cfg.DisableInitialHostLookup = true
// the default timeouts (600ms) are sometimes too short in CI and cause
// PRs being tested to flake due to this integration.
cluster.ConnectTimeout = 2 * time.Second
cluster.Timeout = 2 * time.Second
return cluster
cfg.ConnectTimeout = 2 * time.Second
cfg.Timeout = 2 * time.Second
}

// TestMain sets up the Keyspace and table if they do not exist
@@ -90,6 +100,7 @@ func TestErrorWrapper(t *testing.T) {
assert.Equal(span.Tag(ext.Component), "gocql/gocql")
assert.Equal(span.Tag(ext.SpanKind), ext.SpanKindClient)
assert.Equal(span.Tag(ext.DBSystem), "cassandra")
assert.NotContains(span.Tags(), ext.CassandraContactPoints)

if iter.Host() != nil {
assert.Equal(span.Tag(ext.TargetPort), "9042")
@@ -136,6 +147,7 @@ func TestChildWrapperSpan(t *testing.T) {
assert.Equal(childSpan.Tag(ext.Component), "gocql/gocql")
assert.Equal(childSpan.Tag(ext.SpanKind), ext.SpanKindClient)
assert.Equal(childSpan.Tag(ext.DBSystem), "cassandra")
assert.NotContains(childSpan.Tags(), ext.CassandraContactPoints)

if iter.Host() != nil {
assert.Equal(childSpan.Tag(ext.TargetPort), "9042")
@@ -317,6 +329,7 @@ func TestIterScanner(t *testing.T) {
assert.Equal(childSpan.Tag(ext.Component), "gocql/gocql")
assert.Equal(childSpan.Tag(ext.SpanKind), ext.SpanKindClient)
assert.Equal(childSpan.Tag(ext.DBSystem), "cassandra")
assert.NotContains(childSpan.Tags(), ext.CassandraContactPoints)
}

func TestBatch(t *testing.T) {
@@ -362,6 +375,135 @@ func TestBatch(t *testing.T) {
assert.Equal(childSpan.Tag(ext.Component), "gocql/gocql")
assert.Equal(childSpan.Tag(ext.SpanKind), ext.SpanKindClient)
assert.Equal(childSpan.Tag(ext.DBSystem), "cassandra")
assert.NotContains(childSpan.Tags(), ext.CassandraContactPoints)
}

func TestCassandraContactPoints(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

cluster := NewCluster([]string{cassandraHost, "127.0.0.1:9043"})
updateTestClusterConfig(cluster.ClusterConfig)

session, err := cluster.CreateSession()
require.NoError(t, err)
q := session.Query("CREATE KEYSPACE IF NOT EXISTS trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
err = q.Iter().Close()
require.NoError(t, err)

spans := mt.FinishedSpans()
require.Len(t, spans, 1)
span := spans[0]

assert.Equal(span.OperationName(), "cassandra.query")
assert.Equal(span.Tag(ext.CassandraContactPoints), "127.0.0.1:9042,127.0.0.1:9043")

mt.Reset()

tb := session.NewBatch(gocql.UnloggedBatch)
stmt := "INSERT INTO trace.person (name, age, description) VALUES (?, ?, ?)"
tb.Query(stmt, "Kate", 80, "Cassandra's sister running in kubernetes")
tb.Query(stmt, "Lucas", 60, "Another person")
err = tb.WithContext(context.Background()).WithTimestamp(time.Now().Unix() * 1e3).ExecuteBatch(session.Session)
require.NoError(t, err)

spans = mt.FinishedSpans()
require.Len(t, spans, 1)
span = spans[0]

assert.Equal(span.OperationName(), "cassandra.batch")
assert.Equal(span.Tag(ext.CassandraContactPoints), "127.0.0.1:9042,127.0.0.1:9043")
}

func TestWithWrapOptions(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

cluster := newTracedCassandraCluster(WithServiceName("test-service"), WithResourceName("cluster-resource"))

session, err := cluster.CreateSession()
require.NoError(t, err)
q := session.Query("CREATE KEYSPACE IF NOT EXISTS trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
q = q.WithWrapOptions(WithResourceName("test-resource"), WithCustomTag("custom_tag", "value"))
err = q.Iter().Close()
require.NoError(t, err)

spans := mt.FinishedSpans()
require.Len(t, spans, 1)
span := spans[0]

assert.Equal(span.OperationName(), "cassandra.query")
assert.Equal(span.Tag(ext.CassandraContactPoints), "127.0.0.1:9042")
assert.Equal(span.Tag(ext.ServiceName), "test-service")
assert.Equal(span.Tag(ext.ResourceName), "test-resource")
assert.Equal(span.Tag("custom_tag"), "value")

mt.Reset()

tb := session.NewBatch(gocql.UnloggedBatch)
stmt := "INSERT INTO trace.person (name, age, description) VALUES (?, ?, ?)"
tb.Query(stmt, "Kate", 80, "Cassandra's sister running in kubernetes")
tb.Query(stmt, "Lucas", 60, "Another person")
tb = tb.WithContext(context.Background()).WithTimestamp(time.Now().Unix() * 1e3)
tb = tb.WithWrapOptions(WithResourceName("test-resource"), WithCustomTag("custom_tag", "value"))

err = tb.ExecuteBatch(session.Session)
require.NoError(t, err)

spans = mt.FinishedSpans()
require.Len(t, spans, 1)
span = spans[0]

assert.Equal(span.OperationName(), "cassandra.batch")
assert.Equal(span.Tag(ext.CassandraContactPoints), "127.0.0.1:9042")
assert.Equal(span.Tag(ext.ServiceName), "test-service")
assert.Equal(span.Tag(ext.ResourceName), "test-resource")
assert.Equal(span.Tag("custom_tag"), "value")
}

func TestWithCustomTag(t *testing.T) {
cluster := newCassandraCluster()
cluster.Keyspace = "trace"
session, err := cluster.CreateSession()
require.NoError(t, err)

t.Run("WrapQuery", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

q := session.Query("CREATE KEYSPACE IF NOT EXISTS trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
iter := WrapQuery(q, WithCustomTag("custom_tag", "value")).Iter()
err = iter.Close()
require.NoError(t, err)

spans := mt.FinishedSpans()
require.Len(t, spans, 1)

s0 := spans[0]
assert.Equal(t, "cassandra.query", s0.OperationName())
assert.Equal(t, "value", s0.Tag("custom_tag"))
})
t.Run("WrapBatch", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

b := session.NewBatch(gocql.UnloggedBatch)
tb := WrapBatch(b, WithCustomTag("custom_tag", "value"))
stmt := "INSERT INTO trace.person (name, age, description) VALUES (?, ?, ?)"
tb.Query(stmt, "Kate", 80, "Cassandra's sister running in kubernetes")
tb.Query(stmt, "Lucas", 60, "Another person")
err = tb.WithTimestamp(time.Now().Unix() * 1e3).ExecuteBatch(session)
require.NoError(t, err)

spans := mt.FinishedSpans()
require.Len(t, spans, 1)

s0 := spans[0]
assert.Equal(t, "cassandra.batch", s0.OperationName())
assert.Equal(t, "value", s0.Tag("custom_tag"))
})
}

func TestNamingSchema(t *testing.T) {
@@ -373,23 +515,22 @@ func TestNamingSchema(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

cluster := newCassandraCluster()
cluster := newTracedCassandraCluster(opts...)
session, err := cluster.CreateSession()
require.NoError(t, err)

stmt := "INSERT INTO trace.person (name, age, description) VALUES (?, ?, ?)"

// generate query span
q := session.Query(stmt, "name", 30, "description")
err = WrapQuery(q, opts...).Exec()
err = session.Query(stmt, "name", 30, "description").Exec()
require.NoError(t, err)

// generate batch span
b := session.NewBatch(gocql.UnloggedBatch)
tb := WrapBatch(b, opts...)
tb := session.NewBatch(gocql.UnloggedBatch)

tb.Query(stmt, "Kate", 80, "Cassandra's sister running in kubernetes")
tb.Query(stmt, "Lucas", 60, "Another person")
err = tb.ExecuteBatch(session)
err = tb.ExecuteBatch(session.Session)
require.NoError(t, err)

return mt.FinishedSpans()
15 changes: 14 additions & 1 deletion contrib/gocql/gocql/option.go
Original file line number Diff line number Diff line change
@@ -20,12 +20,14 @@ type queryConfig struct {
noDebugStack bool
analyticsRate float64
errCheck func(err error) bool
customTags map[string]interface{}
}

// WrapOption represents an option that can be passed to WrapQuery.
type WrapOption func(*queryConfig)

func defaults(cfg *queryConfig) {
func defaultConfig() *queryConfig {
cfg := &queryConfig{}
cfg.serviceName = namingschema.NewDefaultServiceName(
defaultServiceName,
namingschema.WithOverrideV0(defaultServiceName),
@@ -41,6 +43,7 @@ func defaults(cfg *queryConfig) {
cfg.analyticsRate = math.NaN()
}
cfg.errCheck = func(error) bool { return true }
return cfg
}

// WithServiceName sets the given service name for the returned query.
@@ -115,3 +118,13 @@ func WithErrorCheck(fn func(err error) bool) WrapOption {
cfg.errCheck = fn
}
}

// WithCustomTag will attach the value to the span tagged by the key.
func WithCustomTag(key string, value interface{}) WrapOption {
return func(cfg *queryConfig) {
if cfg.customTags == nil {
cfg.customTags = make(map[string]interface{})
}
cfg.customTags[key] = value
}
}
47 changes: 38 additions & 9 deletions contrib/google.golang.org/grpc/appsec.go
Original file line number Diff line number Diff line change
@@ -9,70 +9,99 @@ import (
"encoding/json"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/grpcsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/sharedsec"

"github.com/DataDog/appsec-internal-go/netip"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)

// UnaryHandler wrapper to use when AppSec is enabled to monitor its execution.
func appsecUnaryHandlerMiddleware(span ddtrace.Span, handler grpc.UnaryHandler) grpc.UnaryHandler {
instrumentation.SetAppSecEnabledTags(span)
return func(ctx context.Context, req interface{}) (interface{}, error) {
var err error
var blocked bool
md, _ := metadata.FromIncomingContext(ctx)
clientIP := setClientIP(ctx, span, md)
ctx, op := grpcsec.StartHandlerOperation(ctx, grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil)
ctx, op := grpcsec.StartHandlerOperation(ctx, grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil, dyngo.NewDataListener(func(a *sharedsec.Action) {
code, e := a.GRPC()(md)
blocked = a.Blocking()
err = status.Error(codes.Code(code), e.Error())
}))
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
if blocked {
op.AddTag(instrumentation.BlockedRequestTag, true)
}
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecEventsTags(ctx, span, events)
}()

if op.Error != nil {
return nil, op.Error
if err != nil {
return nil, err
}

defer grpcsec.StartReceiveOperation(grpcsec.ReceiveOperationArgs{}, op).Finish(grpcsec.ReceiveOperationRes{Message: req})
return handler(ctx, req)
rv, err := handler(ctx, req)
if e, ok := err.(*grpcsec.MonitoringError); ok {
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())
}
return rv, err
}
}

// StreamHandler wrapper to use when AppSec is enabled to monitor its execution.
func appsecStreamHandlerMiddleware(span ddtrace.Span, handler grpc.StreamHandler) grpc.StreamHandler {
instrumentation.SetAppSecEnabledTags(span)
return func(srv interface{}, stream grpc.ServerStream) error {
var err error
var blocked bool
ctx := stream.Context()
md, _ := metadata.FromIncomingContext(ctx)
clientIP := setClientIP(ctx, span, md)

ctx, op := grpcsec.StartHandlerOperation(ctx, grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil)
ctx, op := grpcsec.StartHandlerOperation(ctx, grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil, dyngo.NewDataListener(func(a *sharedsec.Action) {
code, e := a.GRPC()(md)
blocked = a.Blocking()
err = status.Error(codes.Code(code), e.Error())
}))
stream = appsecServerStream{
ServerStream: stream,
handlerOperation: op,
ctx: ctx,
}
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
if blocked {
op.AddTag(instrumentation.BlockedRequestTag, true)
}
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecEventsTags(stream.Context(), span, events)
}()

if op.Error != nil {
return op.Error
if err != nil {
return err
}

return handler(srv, stream)
err = handler(srv, stream)
if e, ok := err.(*grpcsec.MonitoringError); ok {
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())
}
return err
}
}

4 changes: 2 additions & 2 deletions contrib/google.golang.org/grpc/grpc.go
Original file line number Diff line number Diff line change
@@ -53,11 +53,11 @@ func (cfg *config) startSpanOptions(opts ...tracer.StartSpanOption) []tracer.Sta
}

func startSpanFromContext(
ctx context.Context, method, operation, service string, opts ...tracer.StartSpanOption,
ctx context.Context, method, operation string, serviceFn func() string, opts ...tracer.StartSpanOption,
) (ddtrace.Span, context.Context) {
methodElements := strings.SplitN(strings.TrimPrefix(method, "/"), "/", 2)
opts = append(opts,
tracer.ServiceName(service),
tracer.ServiceName(serviceFn()),
tracer.ResourceName(method),
tracer.Tag(tagMethodName, method),
spanTypeRPC,
86 changes: 86 additions & 0 deletions contrib/google.golang.org/grpc/grpc_test.go
Original file line number Diff line number Diff line change
@@ -6,8 +6,12 @@
package grpc

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync/atomic"
"testing"
@@ -23,9 +27,11 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tinylib/msgp/msgp"
context "golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
@@ -1203,3 +1209,83 @@ func BenchmarkUnaryServerInterceptor(b *testing.B) {
}
})
}

type roundTripper struct {
assertSpanFromRequest func(r *http.Request)
}

func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
rt.assertSpanFromRequest(r)
return http.DefaultTransport.RoundTrip(r)
}

func TestIssue2050(t *testing.T) {
// https://github.com/DataDog/dd-trace-go/issues/2050
t.Setenv("DD_SERVICE", "some-dd-service")

spansFound := make(chan bool, 1)

httpClient := &http.Client{
Transport: &roundTripper{
assertSpanFromRequest: func(r *http.Request) {
if r.URL.Path != "/v0.4/traces" {
return
}
req := r.Clone(context.Background())
defer req.Body.Close()

buf, err := io.ReadAll(req.Body)
require.NoError(t, err)

var payload bytes.Buffer
_, err = msgp.UnmarshalAsJSON(&payload, buf)
require.NoError(t, err)

var trace [][]map[string]interface{}
err = json.Unmarshal(payload.Bytes(), &trace)
require.NoError(t, err)

if len(trace) == 0 {
return
}
require.Len(t, trace, 2)
s0 := trace[0][0]
s1 := trace[1][0]

assert.Equal(t, "server", s0["meta"].(map[string]interface{})["span.kind"])
assert.Equal(t, "some-dd-service", s0["service"])

assert.Equal(t, "client", s1["meta"].(map[string]interface{})["span.kind"])
assert.Equal(t, "grpc.client", s1["service"])
close(spansFound)
},
},
}
serverInterceptors := []grpc.ServerOption{
grpc.UnaryInterceptor(UnaryServerInterceptor()),
grpc.StreamInterceptor(StreamServerInterceptor()),
}
clientInterceptors := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(UnaryClientInterceptor()),
grpc.WithStreamInterceptor(StreamClientInterceptor()),
}
rig, err := newRigWithInterceptors(serverInterceptors, clientInterceptors)
require.NoError(t, err)
defer rig.Close()

// call tracer.Start after integration is initialized, to reproduce the issue
tracer.Start(tracer.WithHTTPClient(httpClient))
defer tracer.Stop()

_, err = rig.client.Ping(context.Background(), &FixtureRequest{Name: "pass"})
require.NoError(t, err)

select {
case <-spansFound:
return

case <-time.After(5 * time.Second):
assert.Fail(t, "spans not found")
}
}
21 changes: 17 additions & 4 deletions contrib/google.golang.org/grpc/option.go
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@ import (
"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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"

"google.golang.org/grpc/codes"
@@ -24,7 +26,7 @@ const (
type Option func(*config)

type config struct {
serviceName string
serviceName func() string
spanName string
nonErrorCodes map[codes.Code]bool
traceStreamCalls bool
@@ -60,24 +62,35 @@ func defaults(cfg *config) {
}

func clientDefaults(cfg *config) {
cfg.serviceName = namingschema.NewDefaultServiceName(
sn := namingschema.NewDefaultServiceName(
defaultClientServiceName,
namingschema.WithOverrideV0(defaultClientServiceName),
).GetName()
cfg.serviceName = func() string { return sn }
cfg.spanName = namingschema.NewGRPCClientOp().GetName()
defaults(cfg)
}

func serverDefaults(cfg *config) {
cfg.serviceName = namingschema.NewDefaultServiceName(defaultServerServiceName).GetName()
// We check for a configured service name, so we don't break users who are incorrectly creating their server
// before the call `tracer.Start()`
if globalconfig.ServiceName() != "" {
sn := namingschema.NewDefaultServiceName(defaultServerServiceName).GetName()
cfg.serviceName = func() string { return sn }
} else {
log.Warn("No global service name was detected. GRPC Server may have been created before calling tracer.Start(). Will dynamically fetch service name for every span. " +
"Note this may have a slight performance cost, it is always recommended to start the tracer before initializing any traced packages.\n")
ns := namingschema.NewDefaultServiceName(defaultServerServiceName)
cfg.serviceName = ns.GetName
}
cfg.spanName = namingschema.NewGRPCServerOp().GetName()
defaults(cfg)
}

// WithServiceName sets the given service name for the intercepted client.
func WithServiceName(name string) Option {
return func(cfg *config) {
cfg.serviceName = name
cfg.serviceName = func() string { return name }
}
}

17 changes: 2 additions & 15 deletions contrib/gorilla/mux/mux.go
Original file line number Diff line number Diff line change
@@ -8,8 +8,8 @@ package mux // import "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorilla/mux"

import (
"net/http"
"strings"

httptraceinternal "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
@@ -107,10 +107,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
route, _ = match.Route.GetPathTemplate()
}
spanopts = append(spanopts, r.config.spanOpts...)

if r.config.headerTags {
spanopts = append(spanopts, headerTagsFromRequest(req))
}
spanopts = append(spanopts, httptraceinternal.HeaderTagsFromRequest(req, r.config.headerTags))
resource := r.config.resourceNamer(r, req)
httptrace.TraceAndServe(r.Router, w, req, &httptrace.ServeConfig{
Service: r.config.serviceName,
@@ -148,13 +145,3 @@ func defaultResourceNamer(router *Router, req *http.Request) string {
}
return req.Method + " unknown"
}

func headerTagsFromRequest(req *http.Request) ddtrace.StartSpanOption {
return func(cfg *ddtrace.StartSpanConfig) {
for k := range req.Header {
if !strings.HasPrefix(strings.ToLower(k), "x-datadog-") {
cfg.Tags["http.request.headers."+k] = strings.Join(req.Header.Values(k), ",")
}
}
}
}
Loading