├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml ├── scripts │ ├── pr_precondition.sh │ └── set_version.sh └── workflows │ ├── codeql-analysis.yml │ ├── lint.yaml │ ├── pr-tidy.yaml │ ├── pr.yaml │ ├── release.yaml │ ├── test-compatibility-libpq.yaml │ ├── test-compatibility-mssql.yaml │ ├── test-compatibility-mysql.yaml │ ├── test-compatibility-pgx.yaml │ ├── test-unit.yaml │ └── update-registry.yaml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── attribute.go ├── attribute ├── attribute.go ├── attribute_test.go └── doc.go ├── begin.go ├── begin_internal_test.go ├── codecov.yml ├── conn.go ├── conn_internal_test.go ├── context.go ├── context_test.go ├── doc.go ├── driver.go ├── driver_test.go ├── errors.go ├── errors_internal_test.go ├── exec.go ├── exec_internal_test.go ├── go.mod ├── go.sum ├── internal └── test │ ├── assert │ ├── assert.go │ └── doc.go │ ├── oteltest │ ├── context.go │ ├── doc.go │ ├── metric.go │ ├── suite.go │ └── trace.go │ └── sqlmock │ ├── connector.go │ ├── doc.go │ └── sqlmock.go ├── middleware.go ├── options.go ├── options_internal_test.go ├── ping.go ├── ping_internal_test.go ├── prepare.go ├── prepare_internal_test.go ├── query.go ├── query_internal_test.go ├── recorder.go ├── resources └── fixtures │ ├── metrics │ ├── begin_commit_ok.json │ ├── begin_rollback_ok.json │ ├── custom_ok.json │ ├── exec_ok.json │ ├── ping_ok.json │ ├── prepare_context_exec_context_ok.json │ ├── prepare_context_query_context_ok.json │ ├── query_ok.json │ └── stats.json │ └── traces │ ├── begin_commit.json │ ├── begin_commit_with_error.json │ ├── begin_rollback.json │ ├── begin_rollback_with_error.json │ ├── begin_with_error.json │ ├── custom.json │ ├── exec_no_query.json │ ├── exec_with_affected_rows.json │ ├── exec_with_error.json │ ├── exec_with_last_insert_id.json │ ├── exec_with_query.json │ ├── exec_with_query_args.json │ ├── ping.json │ ├── ping_with_error.json │ ├── prepare_context_exec_context_no_query.json │ ├── prepare_context_exec_context_with_affected_rows.json │ ├── prepare_context_exec_context_with_error.json │ ├── prepare_context_exec_context_with_last_insert_id.json │ ├── prepare_context_exec_context_with_query.json │ ├── prepare_context_exec_context_with_query_args.json │ ├── prepare_context_query_context_no_query.json │ ├── prepare_context_query_context_with_error.json │ ├── prepare_context_query_context_with_query.json │ ├── prepare_context_query_context_with_query_args.json │ ├── prepare_context_query_context_with_rows_close.json │ ├── prepare_context_query_context_with_rows_next.json │ ├── prepare_context_query_context_with_rows_next_close.json │ ├── prepare_context_with_error.json │ ├── query_no_query.json │ ├── query_with_error.json │ ├── query_with_query.json │ ├── query_with_query_args.json │ ├── query_with_rows_close.json │ ├── query_with_rows_next.json │ └── query_with_rows_next_close.json ├── result.go ├── row.go ├── row_internal_test.go ├── statement.go ├── statement_internal_test.go ├── stats.go ├── stats_test.go ├── tests ├── .gherkin-lintrc ├── features │ ├── TestDb.feature │ └── TestTx.feature ├── mssql │ ├── doc.go │ ├── go.mod │ ├── go.sum │ ├── main_test.go │ ├── repository.go │ └── resources │ │ └── migrations │ │ └── 1_init.up.sql ├── mysql │ ├── doc.go │ ├── go.mod │ ├── go.sum │ ├── main_test.go │ ├── repository.go │ └── resources │ │ └── migrations │ │ └── 1_init.up.sql ├── postgres │ ├── doc.go │ ├── go.mod │ ├── go.sum │ ├── main_test.go │ ├── repository.go │ └── resources │ │ └── migrations │ │ └── 1_init.up.sql └── suite │ ├── context.go │ ├── customer.go │ ├── customer │ ├── doc.go │ ├── entity.go │ └── repository.go │ ├── database.go │ ├── doc.go │ ├── go.mod │ ├── go.sum │ ├── observability.go │ ├── options.go │ ├── random.go │ └── suite.go ├── time.go ├── tracer.go ├── tracer_internal_test.go ├── transaction.go ├── transaction_internal_test.go ├── value.go ├── value_internal_test.go ├── version.go └── version_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | insert_final_newline = true 8 | max_line_length = 160 9 | tab_width = 4 10 | trim_trailing_whitespace = true 11 | 12 | [Makefile] 13 | indent_style = space 14 | 15 | [*.feature] 16 | indent_style = space 17 | 18 | [.golangci.yaml] 19 | indent_size = 2 20 | 21 | [{.github/**/*.yaml,.github/**/*.yml}] 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: nhatthm 4 | custom: donate.nhat.me 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | groups: 13 | otel: 14 | patterns: 15 | - "go.opentelemetry.io/*" 16 | 17 | - package-ecosystem: "gomod" 18 | directory: "/tests/suite" 19 | schedule: 20 | interval: "daily" 21 | ignore: 22 | - dependency-name: "go.opentelemetry.io/*" 23 | - dependency-name: "github.com/stretchr/testify" 24 | 25 | - package-ecosystem: "gomod" 26 | directory: "/tests/mssql" 27 | schedule: 28 | interval: "daily" 29 | ignore: 30 | - dependency-name: "go.nhat.io/clock" 31 | - dependency-name: "go.nhat.io/testcontainers-*" 32 | - dependency-name: "go.opentelemetry.io/*" 33 | - dependency-name: "github.com/Masterminds/squirrel" 34 | - dependency-name: "github.com/stretchr/testify" 35 | 36 | - package-ecosystem: "gomod" 37 | directory: "/tests/mysql" 38 | schedule: 39 | interval: "daily" 40 | ignore: 41 | - dependency-name: "go.nhat.io/clock" 42 | - dependency-name: "go.nhat.io/testcontainers-*" 43 | - dependency-name: "go.opentelemetry.io/*" 44 | - dependency-name: "github.com/Masterminds/squirrel" 45 | - dependency-name: "github.com/stretchr/testify" 46 | 47 | - package-ecosystem: "gomod" 48 | directory: "/tests/postgres" 49 | schedule: 50 | interval: "daily" 51 | ignore: 52 | - dependency-name: "go.nhat.io/clock" 53 | - dependency-name: "go.nhat.io/testcontainers-*" 54 | - dependency-name: "go.opentelemetry.io/*" 55 | - dependency-name: "github.com/Masterminds/squirrel" 56 | - dependency-name: "github.com/stretchr/testify" 57 | 58 | - package-ecosystem: "github-actions" 59 | directory: "/" 60 | schedule: 61 | interval: "daily" 62 | -------------------------------------------------------------------------------- /.github/scripts/pr_precondition.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | HEAD_REF=${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} 4 | BASE_REF="master" 5 | 6 | EXECUTE="true" 7 | 8 | make tidy 9 | 10 | if [[ $(git ls-files --modified | wc -l | xargs) -gt 0 ]]; then 11 | EXECUTE="false" 12 | fi 13 | 14 | echo "passed=$EXECUTE" >> "$GITHUB_OUTPUT" 15 | -------------------------------------------------------------------------------- /.github/scripts/set_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | NO_COLOR="\033[0m" 6 | ERROR_COLOR="\033[31;01m" 7 | 8 | function lastVersion() { 9 | git tag --sort=committerdate | tail -1 | tr -d 'v' 10 | } 11 | 12 | function toInt() { 13 | echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' 14 | } 15 | 16 | function setVersion() { 17 | local version=$1 18 | 19 | if [[ -z "$version" ]]; then 20 | echo -e "${ERROR_COLOR}No version specified${NO_COLOR}" 21 | exit 1 22 | fi 23 | 24 | if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 25 | echo -e "${ERROR_COLOR}Invalid version '${version}'${NO_COLOR}" 26 | exit 1 27 | fi 28 | 29 | # shellcheck disable=SC2155 30 | local last=$(lastVersion) 31 | 32 | if [[ -z "$last" ]]; then 33 | last='0.0.0' 34 | fi 35 | 36 | # Compare version and last version. 37 | if [[ $(toInt "$version") -le $(toInt "$last") ]]; then 38 | echo -e "${ERROR_COLOR}Version '${version}' is not greater than last version '${last}'${NO_COLOR}" 39 | exit 1 40 | fi 41 | 42 | echo "Last version: '${last}'" 43 | echo "New version '${version}'" 44 | 45 | # Update version in go file. 46 | cat <version.go 47 | // Code generated by release workflow. DO NOT EDIT. 48 | 49 | package otelsql 50 | 51 | // Version is the current release version of the otelsql instrumentation. 52 | func Version() string { 53 | return "$version" 54 | } 55 | 56 | // SemVersion is the semantic version to be supplied to tracer/meter creation. 57 | func SemVersion() string { 58 | return "semver:" + Version() 59 | } 60 | EOF 61 | 62 | # Send version to the next job. 63 | echo "VERSION=v${version}" >>"$GITHUB_ENV" 64 | } 65 | 66 | setVersion "$@" 67 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | --- 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ master ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ master ] 21 | schedule: 22 | - cron: '33 10 * * 6' 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | analyze: 30 | name: Analyze 31 | runs-on: ubuntu-latest 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | language: [ 'go' ] 41 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 42 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 43 | 44 | steps: 45 | - name: Setup 46 | uses: nhatthm/gh-actions/find-go-version@master 47 | with: 48 | go-version-file: "" 49 | 50 | - name: Run CodeQL 51 | uses: nhatthm/gh-actions/codeql@master 52 | with: 53 | language: ${{ matrix.language }} 54 | go-version: ${{ env.GO_LATEST_VERSION }} 55 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint-go: 15 | name: lint 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | module: [ ".", "tests/suite", "tests/mssql", "tests/mysql", "tests/postgres" ] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: nhatthm/gh-actions/checkout@master 24 | 25 | - name: Setup 26 | uses: nhatthm/gh-actions/find-go-version@master 27 | 28 | - name: Install Go 29 | uses: nhatthm/gh-actions/setup-go@master 30 | with: 31 | go-version: ${{ env.GO_LATEST_VERSION }} 32 | 33 | - name: Lint 34 | uses: nhatthm/gh-actions/golangci-lint@master 35 | with: 36 | working-directory: ${{ matrix.module }} 37 | args: --timeout=5m 38 | 39 | lint-gherkin: 40 | name: lint 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout code 44 | uses: nhatthm/gh-actions/checkout@master 45 | 46 | - name: Lint 47 | uses: nhatthm/gh-actions/gherkin-lint@master 48 | with: 49 | feature_files: tests/features/* 50 | config_file: tests/.gherkin-lintrc 51 | -------------------------------------------------------------------------------- /.github/workflows/pr-tidy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr-tidy 3 | 4 | on: 5 | pull_request_target: 6 | 7 | env: 8 | GO111MODULE: "on" 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | tidy: 16 | runs-on: ubuntu-latest 17 | if: ${{ startsWith(github.head_ref, 'dependabot/go_modules/') }} 18 | steps: 19 | - name: Checkout code 20 | uses: nhatthm/gh-actions/checkout@master 21 | with: 22 | token: ${{ secrets.PUSH_TOKEN }} 23 | ref: ${{ github.head_ref }} 24 | 25 | - name: Find Go version 26 | uses: nhatthm/gh-actions/find-go-version@master 27 | 28 | - name: Install Go 29 | uses: nhatthm/gh-actions/setup-go@master 30 | with: 31 | go-version: ${{ env.GO_VERSION }} 32 | 33 | - name: Tidy 34 | run: | 35 | make tidy 36 | 37 | - name: Setup GPG 38 | id: setup-gpg 39 | uses: nhatthm/gh-actions/import-gpg@master 40 | with: 41 | gpg_private_key: ${{ secrets.PUSH_PRIVATE_KEY }} 42 | passphrase: ${{ secrets.PUSH_SECRET }} 43 | git_config_global: true 44 | git_user_signingkey: true 45 | git_commit_gpgsign: true 46 | git_tag_gpgsign: false 47 | 48 | - name: Add and Commit 49 | uses: nhatthm/gh-actions/git-add-and-commit@master 50 | with: 51 | message: "go mod tidy" 52 | push: true 53 | author_name: ${{ steps.setup-gpg.outputs.name }} 54 | author_email: ${{ steps.setup-gpg.outputs.email }} 55 | committer_name: ${{ steps.setup-gpg.outputs.name }} 56 | committer_email: ${{ steps.setup-gpg.outputs.email }} 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | run-name: Release ${{ inputs.version }} 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: | 10 | Version to release. Must be greater than the last version. 11 | required: true 12 | 13 | jobs: 14 | release: 15 | name: release 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: nhatthm/gh-actions/checkout@master 20 | with: 21 | token: ${{ secrets.RELEASE_TOKEN }} 22 | ref: master 23 | fetch-depth: 0 24 | 25 | - name: set-version 26 | run: .github/scripts/set_version.sh "${{ github.event.inputs.version }}" 27 | 28 | - name: Setup GPG 29 | id: setup-gpg 30 | uses: nhatthm/gh-actions/import-gpg@master 31 | with: 32 | gpg_private_key: ${{ secrets.RELEASE_PRIVATE_KEY }} 33 | passphrase: ${{ secrets.RELEASE_SECRET }} 34 | git_config_global: true 35 | git_user_signingkey: true 36 | git_commit_gpgsign: true 37 | git_tag_gpgsign: false 38 | 39 | - name: Add and Commit 40 | uses: nhatthm/gh-actions/git-add-and-commit@master 41 | with: 42 | add: version.go 43 | message: "Release ${{ env.VERSION }}" 44 | push: true 45 | tag: "${{ env.VERSION }}" 46 | author_name: ${{ steps.setup-gpg.outputs.name }} 47 | author_email: ${{ steps.setup-gpg.outputs.email }} 48 | committer_name: ${{ steps.setup-gpg.outputs.name }} 49 | committer_email: ${{ steps.setup-gpg.outputs.email }} 50 | 51 | - uses: nhatthm/gh-actions/github-release@master 52 | with: 53 | name: "${{ env.VERSION }}" 54 | tag_name: "${{ env.VERSION }}" 55 | generate_release_notes: true 56 | -------------------------------------------------------------------------------- /.github/workflows/test-compatibility-libpq.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: compatibility-test-libpq 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: "on" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | go-version: ${{ steps.find-go-version.outputs.go-version }} 21 | go-latest-version: ${{ steps.find-go-version.outputs.go-latest-version }} 22 | go-supported-versions: ${{ steps.find-go-version.outputs.go-supported-versions }} 23 | steps: 24 | - name: Checkout code 25 | uses: nhatthm/gh-actions/checkout@master 26 | 27 | - id: find-go-version 28 | name: Find Go version 29 | uses: nhatthm/gh-actions/find-go-version@master 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | go-version: ${{ fromJson(needs.setup.outputs.go-supported-versions) }} 36 | arch: [ "386", amd64 ] 37 | postgres-version: [ "13", "14", "15", "16", "17" ] 38 | runs-on: ubuntu-latest 39 | needs: [setup] 40 | steps: 41 | - name: Checkout code 42 | uses: nhatthm/gh-actions/checkout@master 43 | 44 | - name: Install Go 45 | uses: nhatthm/gh-actions/setup-go@master 46 | with: 47 | go-version: ${{ matrix.go-version }} 48 | cache-key: ${{ runner.os }}-go-${{ matrix.go-version }}-postgres-cache-${{ hashFiles('**/go.sum') }} 49 | cache-restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-postgres-cache 50 | 51 | - name: Test 52 | id: test 53 | env: 54 | GOARCH: ${{ matrix.arch }} 55 | POSTGRES_VERSION: ${{ matrix.postgres-version }}-alpine 56 | POSTGRES_DRIVER: postgres 57 | run: | 58 | make test-compatibility-postgres 59 | -------------------------------------------------------------------------------- /.github/workflows/test-compatibility-mssql.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: compatibility-test-mssql 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: "on" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | go-version: ${{ steps.find-go-version.outputs.go-version }} 21 | go-latest-version: ${{ steps.find-go-version.outputs.go-latest-version }} 22 | go-supported-versions: ${{ steps.find-go-version.outputs.go-supported-versions }} 23 | steps: 24 | - name: Checkout code 25 | uses: nhatthm/gh-actions/checkout@master 26 | 27 | - id: find-go-version 28 | name: Find Go version 29 | uses: nhatthm/gh-actions/find-go-version@master 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | go-version: ${{ fromJson(needs.setup.outputs.go-supported-versions) }} 36 | arch: [ "386", amd64 ] 37 | mssql-version: [ "2019" ] 38 | runs-on: ubuntu-latest 39 | needs: [setup] 40 | steps: 41 | - name: Checkout code 42 | uses: nhatthm/gh-actions/checkout@master 43 | 44 | - name: Install Go 45 | uses: nhatthm/gh-actions/setup-go@master 46 | with: 47 | go-version: ${{ matrix.go-version }} 48 | cache-key: ${{ runner.os }}-go-${{ matrix.go-version }}-mssql-cache-${{ hashFiles('**/go.sum') }} 49 | cache-restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-mssql-cache 50 | 51 | - name: Test 52 | id: test 53 | env: 54 | GOARCH: ${{ matrix.arch }} 55 | MSSQL_VERSION: ${{ matrix.mssql-version }}-latest 56 | run: | 57 | make test-compatibility-mssql 58 | -------------------------------------------------------------------------------- /.github/workflows/test-compatibility-mysql.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: compatibility-test-mysql 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: "on" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | go-version: ${{ steps.find-go-version.outputs.go-version }} 21 | go-latest-version: ${{ steps.find-go-version.outputs.go-latest-version }} 22 | go-supported-versions: ${{ steps.find-go-version.outputs.go-supported-versions }} 23 | steps: 24 | - name: Checkout code 25 | uses: nhatthm/gh-actions/checkout@master 26 | 27 | - id: find-go-version 28 | name: Find Go version 29 | uses: nhatthm/gh-actions/find-go-version@master 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | go-version: ${{ fromJson(needs.setup.outputs.go-supported-versions) }} 36 | arch: [ "386", amd64 ] 37 | mysql-version: [ "8" ] 38 | runs-on: ubuntu-latest 39 | needs: [setup] 40 | steps: 41 | - name: Checkout code 42 | uses: nhatthm/gh-actions/checkout@master 43 | 44 | - name: Install Go 45 | uses: nhatthm/gh-actions/setup-go@master 46 | with: 47 | go-version: ${{ matrix.go-version }} 48 | cache-key: ${{ runner.os }}-go-${{ matrix.go-version }}-mysql-cache-${{ hashFiles('**/go.sum') }} 49 | cache-restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-mysql-cache 50 | 51 | - name: Test 52 | id: test 53 | env: 54 | GOARCH: ${{ matrix.arch }} 55 | MYSQL_VERSION: ${{ matrix.mysql-version }} 56 | run: | 57 | make test-compatibility-mysql 58 | -------------------------------------------------------------------------------- /.github/workflows/test-compatibility-pgx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: compatibility-test-pgx 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: "on" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | go-version: ${{ steps.find-go-version.outputs.go-version }} 21 | go-latest-version: ${{ steps.find-go-version.outputs.go-latest-version }} 22 | go-supported-versions: ${{ steps.find-go-version.outputs.go-supported-versions }} 23 | steps: 24 | - name: Checkout code 25 | uses: nhatthm/gh-actions/checkout@master 26 | 27 | - id: find-go-version 28 | name: Find Go version 29 | uses: nhatthm/gh-actions/find-go-version@master 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | go-version: ${{ fromJson(needs.setup.outputs.go-supported-versions) }} 36 | arch: [ "386", amd64 ] 37 | postgres-version: [ "13", "14", "15", "16", "17" ] 38 | pgx-version: [ "v4", "v5"] 39 | runs-on: ubuntu-latest 40 | needs: [setup] 41 | steps: 42 | - name: Checkout code 43 | uses: nhatthm/gh-actions/checkout@master 44 | 45 | - name: Install Go 46 | uses: nhatthm/gh-actions/setup-go@master 47 | with: 48 | go-version: ${{ matrix.go-version }} 49 | cache-key: ${{ runner.os }}-go-${{ matrix.go-version }}-postgres-cache-${{ hashFiles('**/go.sum') }} 50 | cache-restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-postgres-cache 51 | 52 | - name: Test 53 | id: test 54 | env: 55 | GOARCH: ${{ matrix.arch }} 56 | POSTGRES_VERSION: ${{ matrix.postgres-version }}-alpine 57 | POSTGRES_DRIVER: pgx/${{ matrix.pgx-version }} 58 | run: | 59 | make test-compatibility-postgres 60 | -------------------------------------------------------------------------------- /.github/workflows/test-unit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: unit-test 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: "on" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | go-version: ${{ steps.find-go-version.outputs.go-version }} 21 | go-latest-version: ${{ steps.find-go-version.outputs.go-latest-version }} 22 | go-supported-versions: ${{ steps.find-go-version.outputs.go-supported-versions }} 23 | steps: 24 | - name: Checkout code 25 | uses: nhatthm/gh-actions/checkout@master 26 | 27 | - id: find-go-version 28 | name: Find Go version 29 | uses: nhatthm/gh-actions/find-go-version@master 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: [ ubuntu-latest, macos-latest, windows-latest ] 36 | go-version: ${{ fromJson(needs.setup.outputs.go-supported-versions) }} 37 | arch: [ "386", amd64 ] 38 | exclude: 39 | - os: macos-latest 40 | arch: "386" 41 | runs-on: ${{ matrix.os }} 42 | needs: [setup] 43 | env: 44 | GO_LATEST_VERSION: ${{ needs.setup.outputs.go-latest-version }} 45 | steps: 46 | - name: Checkout code 47 | uses: nhatthm/gh-actions/checkout@master 48 | 49 | - name: Install Go 50 | uses: nhatthm/gh-actions/setup-go@master 51 | with: 52 | go-version: ${{ matrix.go-version }} 53 | 54 | - name: Test 55 | id: test 56 | env: 57 | GOARCH: ${{ matrix.arch }} 58 | run: | 59 | make test-unit 60 | 61 | - name: Upload code coverage (unit) 62 | if: ${{ matrix.go-version == env.GO_LATEST_VERSION }} 63 | uses: nhatthm/gh-actions/codecov@master 64 | with: 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | files: ./unit.coverprofile 67 | flags: unittests-${{ runner.os }}-${{ runner.arch }} 68 | -------------------------------------------------------------------------------- /.github/workflows/update-registry.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'update-registry' 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | workflow_dispatch: 11 | 12 | jobs: 13 | notify: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: nhatthm/gh-actions/checkout@master 18 | 19 | - name: Notify registries 20 | uses: nhatthm/gh-actions/notify-go-registries@master 21 | with: 22 | token: ${{ secrets.REGISTRY_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin 15 | # Dependency directories (remove the comment below to include it) 16 | vendor 17 | 18 | *.coverprofile 19 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: true 4 | linters: 5 | default: all 6 | disable: 7 | - contextcheck 8 | - copyloopvar 9 | - depguard 10 | - err113 11 | - exhaustruct 12 | - forbidigo 13 | - forcetypeassert 14 | - funcorder 15 | - gochecknoglobals 16 | - gomoddirectives 17 | - intrange 18 | - ireturn 19 | - lll 20 | - mnd 21 | - nolintlint 22 | - nonamedreturns 23 | - paralleltest 24 | - perfsprint 25 | - rowserrcheck 26 | - sqlclosecheck 27 | - tagalign 28 | - tagliatelle 29 | - testifylint 30 | - testpackage 31 | - varnamelen 32 | - wastedassign 33 | - wrapcheck 34 | settings: 35 | dupl: 36 | threshold: 100 37 | errcheck: 38 | check-type-assertions: true 39 | check-blank: true 40 | gocyclo: 41 | min-complexity: 20 42 | misspell: 43 | locale: US 44 | unparam: 45 | check-exported: true 46 | exclusions: 47 | generated: lax 48 | rules: 49 | - linters: 50 | - containedctx 51 | - dupl 52 | - execinquery 53 | - funlen 54 | - goconst 55 | - maintidx 56 | - mnd 57 | - nilnil 58 | - noctx 59 | - rowserrcheck 60 | path: _test.go 61 | paths: 62 | - third_party$ 63 | - builtin$ 64 | - examples$ 65 | formatters: 66 | enable: 67 | - gofmt 68 | - gofumpt 69 | - goimports 70 | exclusions: 71 | generated: lax 72 | paths: 73 | - third_party$ 74 | - builtin$ 75 | - examples$ 76 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE_NAME=otelsql 2 | 3 | VENDOR_DIR = vendor 4 | 5 | GOLANGCI_LINT_VERSION ?= v2.1.6 6 | 7 | GO ?= go 8 | GOLANGCI_LINT ?= $(shell $(GO) env GOPATH)/bin/golangci-lint-$(GOLANGCI_LINT_VERSION) 9 | GHERKIN_LINT ?= gherkin-lint 10 | 11 | TEST_FLAGS ?= -race 12 | COMPATIBILITY_TEST ?= postgres 13 | 14 | GITHUB_OUTPUT ?= /dev/null 15 | 16 | ifeq ($(GOARCH), 386) 17 | TEST_FLAGS = 18 | endif 19 | 20 | goModules := $(shell find . -name 'go.mod' | xargs dirname) 21 | tidyGoModules := $(subst -.,-module,$(subst /,-,$(addprefix tidy-,$(goModules)))) 22 | updateGoModules := $(subst -.,-module,$(subst /,-,$(addprefix update-,$(goModules)))) 23 | lintGoModules := $(subst -.,-module,$(subst /,-,$(addprefix lint-,$(goModules)))) 24 | compatibilityTests := $(addprefix test-compatibility-,$(filter-out suite,$(subst ./,,$(shell cd tests;find . -name 'go.mod' | xargs dirname)))) 25 | 26 | .PHONY: help 27 | help: 28 | @make -qpRr | egrep -e '^[a-z].*:$$' | sed -e 's~:~~g' | sort 29 | 30 | .PHONY: $(VENDOR_DIR) 31 | $(VENDOR_DIR): 32 | @mkdir -p $(VENDOR_DIR) 33 | @$(GO) mod tidy 34 | @$(GO) mod vendor 35 | 36 | .PHONY: $(lintGoModules) 37 | $(lintGoModules): $(GOLANGCI_LINT) 38 | $(eval GO_MODULE := "$(subst lint/module,.,$(subst -,/,$(subst lint-module-,,$@)))") 39 | 40 | @echo ">> module: $(GO_MODULE)" 41 | @cd "$(GO_MODULE)"; $(GOLANGCI_LINT) run 42 | 43 | .PHONY: lint 44 | lint: $(lintGoModules) 45 | 46 | .PHONY: $(tidyGoModules) 47 | $(tidyGoModules): 48 | $(eval GO_MODULE := "$(subst tidy/module,.,$(subst -,/,$(subst tidy-module-,,$@)))") 49 | 50 | @echo ">> module: $(GO_MODULE)" 51 | @cd "$(GO_MODULE)"; $(GO) mod tidy 52 | 53 | .PHONY: tidy 54 | tidy: $(tidyGoModules) 55 | 56 | .PHONY: $(updateGoModules) 57 | $(updateGoModules): 58 | $(eval GO_MODULE := "$(subst update/module,.,$(subst -,/,$(subst update-module-,,$@)))") 59 | 60 | @echo ">> module: $(GO_MODULE)" 61 | @cd "$(GO_MODULE)"; $(GO) get -u ./... 62 | 63 | .PHONY: update 64 | update: $(updateGoModules) 65 | 66 | ## Run unit tests 67 | .PHONY: test-unit 68 | test-unit: 69 | @echo ">> unit test" 70 | @$(GO) test -coverprofile=unit.coverprofile -covermode=atomic $(TEST_FLAGS) ./... 71 | @echo 72 | 73 | .PHONY: $(compatibilityTests) 74 | $(compatibilityTests): 75 | $(eval COMPATIBILITY_TEST := "$(subst test-compatibility-,,$@)") 76 | @echo ">> compatibility test: $(COMPATIBILITY_TEST)" 77 | @cd "tests/$(COMPATIBILITY_TEST)"; $(GO) test -v $(TEST_FLAGS) ./... 78 | @echo 79 | 80 | .PHONY: test-compatibility 81 | test-compatibility: $(compatibilityTests) 82 | 83 | .PHONY: test 84 | test: test-unit test-compatibility 85 | 86 | .PHONY: $(GITHUB_OUTPUT) 87 | $(GITHUB_OUTPUT): 88 | @echo "MODULE_NAME=$(MODULE_NAME)" >> "$@" 89 | @echo "GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION)" >> "$@" 90 | 91 | $(GOLANGCI_LINT): 92 | @echo "$(OK_COLOR)==> Installing golangci-lint $(GOLANGCI_LINT_VERSION)$(NO_COLOR)"; \ 93 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin "$(GOLANGCI_LINT_VERSION)" 94 | @mv ./bin/golangci-lint $(GOLANGCI_LINT) 95 | -------------------------------------------------------------------------------- /attribute.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "go.opentelemetry.io/otel/attribute" 5 | ) 6 | 7 | const ( 8 | // Type: string. 9 | // Required: No. 10 | dbInstance = attribute.Key("db.instance") 11 | 12 | // Type: string. 13 | // Required: No. 14 | dbSQLStatus = attribute.Key("db.sql.status") 15 | // Type: string. 16 | // Required: No. 17 | dbSQLError = attribute.Key("db.sql.error") 18 | // Type: int64. 19 | // Required: No. 20 | dbSQLRowsNextSuccessCount = attribute.Key("db.sql.rows_next.success_count") 21 | // Type: string. 22 | // Required: No. 23 | dbSQLRowsNextLatencyAvg = attribute.Key("db.sql.rows_next.latency_avg") 24 | ) 25 | 26 | var ( 27 | dbSQLStatusOK = dbSQLStatus.String("OK") 28 | dbSQLStatusERROR = dbSQLStatus.String("ERROR") 29 | ) 30 | -------------------------------------------------------------------------------- /attribute/attribute.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "go.opentelemetry.io/otel/attribute" 12 | ) 13 | 14 | const ( 15 | _maxStringValueLength = 256 16 | _shortenedPattern = "... (more than 256 chars)" 17 | ) 18 | 19 | // FromNamedValue converts driver.NamedValue to attribute.KeyValue. 20 | func FromNamedValue(arg driver.NamedValue) attribute.KeyValue { 21 | return KeyValue(KeyFromNamedValue(arg), arg.Value) 22 | } 23 | 24 | // KeyFromNamedValue returns an attribute.Key from a given driver.NamedValue. 25 | func KeyFromNamedValue(arg driver.NamedValue) attribute.Key { 26 | var sb strings.Builder 27 | 28 | sb.WriteString("db.sql.args.") 29 | 30 | if arg.Name != "" { 31 | sb.WriteString(arg.Name) 32 | } else { 33 | sb.WriteString(strconv.Itoa(arg.Ordinal)) 34 | } 35 | 36 | return attribute.Key(sb.String()) 37 | } 38 | 39 | // KeyValue returns an attribute.KeyValue from a given value. 40 | // nolint: cyclop 41 | func KeyValue(key attribute.Key, val any) attribute.KeyValue { 42 | switch v := val.(type) { 43 | case nil: 44 | return key.String("") 45 | 46 | case int: 47 | return key.Int(v) 48 | 49 | case int64: 50 | return key.Int64(v) 51 | 52 | case float64: 53 | return key.Float64(v) 54 | 55 | case bool: 56 | return key.Bool(v) 57 | 58 | case []byte: 59 | return key.String(shortenString(string(v))) 60 | 61 | case string: 62 | return key.String(shortenString(v)) 63 | 64 | case []int: 65 | return key.IntSlice(v) 66 | 67 | case []int64: 68 | return key.Int64Slice(v) 69 | 70 | case []float64: 71 | return key.Float64Slice(v) 72 | 73 | case []bool: 74 | return key.BoolSlice(v) 75 | 76 | case *int, *int64, *float64, *bool, *string: 77 | val := reflect.ValueOf(v) 78 | 79 | if val.IsNil() { 80 | return key.String("") 81 | } 82 | 83 | return KeyValue(key, val.Elem().Interface()) 84 | 85 | case time.Duration: 86 | return KeyValueDuration(key, v) 87 | 88 | default: 89 | return key.String(shortenString(fmt.Sprintf("%v", v))) 90 | } 91 | } 92 | 93 | // KeyValueDuration converts time.Duration to attribute.KeyValue. 94 | func KeyValueDuration(key attribute.Key, d time.Duration) attribute.KeyValue { 95 | if time.Microsecond <= d && d < time.Millisecond { 96 | var sb strings.Builder 97 | 98 | sb.WriteString(strconv.FormatInt(d.Microseconds(), 10)) 99 | sb.WriteString("us") 100 | 101 | return key.String(sb.String()) 102 | } 103 | 104 | return key.String(d.String()) 105 | } 106 | 107 | func shortenString(s string) string { 108 | runes := []rune(s) 109 | 110 | if len(runes) <= _maxStringValueLength { 111 | return s 112 | } 113 | 114 | end := _maxStringValueLength - len(_shortenedPattern) 115 | sb := strings.Builder{} 116 | 117 | sb.Grow(_maxStringValueLength) 118 | sb.WriteString(string(runes[:end])) 119 | sb.WriteString(_shortenedPattern) 120 | 121 | return sb.String() 122 | } 123 | -------------------------------------------------------------------------------- /attribute/doc.go: -------------------------------------------------------------------------------- 1 | // Package attribute provides functionalities for converting database values to attributes. 2 | package attribute 3 | -------------------------------------------------------------------------------- /begin.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | ) 7 | 8 | const ( 9 | metricMethodBegin = "go.sql.begin" 10 | traceMethodBegin = "begin_transaction" 11 | ) 12 | 13 | // beginFuncMiddleware is a type for beginFunc middleware. 14 | type beginFuncMiddleware = middleware[beginFunc] 15 | 16 | // beginFunc is a callback for beginFunc. 17 | type beginFunc func(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) 18 | 19 | // nopBegin pings nothing. 20 | func nopBegin(_ context.Context, _ driver.TxOptions) (driver.Tx, error) { 21 | return nil, nil //nolint: nilnil 22 | } 23 | 24 | func ensureBegin(conn driver.Conn) beginFunc { 25 | if b, ok := conn.(driver.ConnBeginTx); ok { 26 | return b.BeginTx 27 | } 28 | 29 | return func(_ context.Context, _ driver.TxOptions) (driver.Tx, error) { 30 | return conn.Begin() // nolint: staticcheck 31 | } 32 | } 33 | 34 | // beginStats records begin stats. 35 | func beginStats(r methodRecorder) beginFuncMiddleware { 36 | return func(next beginFunc) beginFunc { 37 | return func(ctx context.Context, opts driver.TxOptions) (result driver.Tx, err error) { 38 | end := r.Record(ctx, metricMethodBegin) 39 | 40 | defer func() { 41 | end(err) 42 | }() 43 | 44 | return next(ctx, opts) 45 | } 46 | } 47 | } 48 | 49 | // beginTrace traces begin. 50 | func beginTrace(t methodTracer) beginFuncMiddleware { 51 | return func(next beginFunc) beginFunc { 52 | return func(ctx context.Context, opts driver.TxOptions) (result driver.Tx, err error) { 53 | ctx, end := t.Trace(ctx, traceMethodBegin) 54 | 55 | defer func() { 56 | end(err) 57 | }() 58 | 59 | return next(ctx, opts) 60 | } 61 | } 62 | } 63 | 64 | func beginWrapTx(r methodRecorder, t methodTracer) beginFuncMiddleware { 65 | return func(next beginFunc) beginFunc { 66 | return func(ctx context.Context, opts driver.TxOptions) (result driver.Tx, err error) { 67 | tx, err := next(ctx, opts) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | shouldTrace, _ := t.ShouldTrace(ctx) 73 | 74 | return wrapTx(ctx, tx, r, tracerOrNil(t, shouldTrace)), nil 75 | } 76 | } 77 | } 78 | 79 | func makeBeginFuncMiddlewares(r methodRecorder, t methodTracer) []beginFuncMiddleware { 80 | return []beginFuncMiddleware{ 81 | beginStats(r), beginTrace(t), beginWrapTx(r, t), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "features/**/*" 3 | - "internal/**/*" 4 | - "tests/**/*" 5 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "errors" 7 | ) 8 | 9 | type connConfig struct { 10 | pingFuncMiddlewares []pingFuncMiddleware 11 | execContextFuncMiddlewares []execContextFuncMiddleware 12 | queryContextFuncMiddlewares []queryContextFuncMiddleware 13 | beginFuncMiddlewares []beginFuncMiddleware 14 | prepareFuncMiddlewares []prepareContextFuncMiddleware 15 | } 16 | 17 | type conn struct { 18 | ping pingFunc 19 | exec execContextFunc 20 | query queryContextFunc 21 | begin beginFunc 22 | prepare prepareContextFunc 23 | 24 | close func() error 25 | } 26 | 27 | func (c conn) Ping(ctx context.Context) error { 28 | return c.ping(ctx) 29 | } 30 | 31 | // Deprecated: Drivers should implement ExecerContext instead. 32 | func (c conn) Exec(string, []driver.Value) (driver.Result, error) { 33 | return nil, errors.New("otelsql: Exec is deprecated") 34 | } 35 | 36 | func (c conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 37 | return c.exec(ctx, query, args) 38 | } 39 | 40 | // Deprecated: Drivers should implement QueryerContext instead. 41 | func (c conn) Query(string, []driver.Value) (driver.Rows, error) { 42 | return nil, errors.New("otelsql: Query is deprecated") 43 | } 44 | 45 | func (c conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 46 | return c.query(ctx, query, args) 47 | } 48 | 49 | func (c conn) Prepare(query string) (driver.Stmt, error) { 50 | return c.prepare(context.Background(), query) 51 | } 52 | 53 | func (c conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 54 | return c.prepare(ctx, query) 55 | } 56 | 57 | func (c conn) Begin() (driver.Tx, error) { 58 | return c.begin(context.Background(), driver.TxOptions{}) 59 | } 60 | 61 | func (c conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 62 | return c.begin(ctx, opts) 63 | } 64 | 65 | func (c conn) Close() error { 66 | return c.close() 67 | } 68 | 69 | func wrapConn(parent driver.Conn, opt connConfig) driver.Conn { 70 | c := makeConn(parent, opt) 71 | 72 | var ( 73 | n, hasNameValueChecker = parent.(driver.NamedValueChecker) 74 | s, hasSessionResetter = parent.(driver.SessionResetter) 75 | ) 76 | 77 | switch { 78 | default: 79 | // case !hasNameValueChecker && !hasSessionResetter: 80 | return c 81 | 82 | case hasNameValueChecker && !hasSessionResetter: 83 | return struct { 84 | conn 85 | driver.NamedValueChecker 86 | }{c, n} 87 | 88 | case !hasNameValueChecker && hasSessionResetter: 89 | return struct { 90 | conn 91 | driver.SessionResetter 92 | }{c, s} 93 | 94 | case hasNameValueChecker && hasSessionResetter: 95 | return struct { 96 | conn 97 | driver.NamedValueChecker 98 | driver.SessionResetter 99 | }{c, n, s} 100 | } 101 | } 102 | 103 | func makeConn(parent driver.Conn, cfg connConfig) conn { 104 | c := conn{ 105 | ping: nopPing, 106 | exec: skippedExecContext, 107 | query: skippedQueryContext, 108 | close: parent.Close, 109 | } 110 | 111 | if p, ok := parent.(driver.Pinger); ok { 112 | c.ping = chainMiddlewares(cfg.pingFuncMiddlewares, p.Ping) 113 | } 114 | 115 | if p, ok := parent.(driver.ExecerContext); ok { 116 | c.exec = chainMiddlewares(cfg.execContextFuncMiddlewares, p.ExecContext) 117 | } 118 | 119 | if p, ok := parent.(driver.QueryerContext); ok { 120 | c.query = chainMiddlewares(cfg.queryContextFuncMiddlewares, p.QueryContext) 121 | } 122 | 123 | c.begin = chainMiddlewares(cfg.beginFuncMiddlewares, ensureBegin(parent)) 124 | c.prepare = chainMiddlewares(cfg.prepareFuncMiddlewares, ensurePrepareContext(parent)) 125 | 126 | return c 127 | } 128 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import "context" 4 | 5 | type queryCtxKey struct{} 6 | 7 | // QueryFromContext gets the query from context. 8 | func QueryFromContext(ctx context.Context) string { 9 | query, ok := ctx.Value(queryCtxKey{}).(string) 10 | if !ok { 11 | return "" 12 | } 13 | 14 | return query 15 | } 16 | 17 | // ContextWithQuery attaches the query to the parent context. 18 | func ContextWithQuery(ctx context.Context, query string) context.Context { 19 | return context.WithValue(ctx, queryCtxKey{}, query) 20 | } 21 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package otelsql_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "go.nhat.io/otelsql" 10 | ) 11 | 12 | func TestQueryContext(t *testing.T) { 13 | t.Parallel() 14 | 15 | actual := otelsql.QueryFromContext(context.Background()) 16 | assert.Empty(t, actual) 17 | 18 | ctx := otelsql.ContextWithQuery(context.Background(), "SELECT 1") 19 | actual = otelsql.QueryFromContext(ctx) 20 | expected := "SELECT 1" 21 | 22 | assert.Equal(t, expected, actual) 23 | } 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package otelsql provides traces and metrics for database/sql drivers. 2 | package otelsql 3 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import "go.opentelemetry.io/otel" 4 | 5 | func handleErr(err error) { 6 | if err != nil { 7 | otel.Handle(err) 8 | } 9 | } 10 | 11 | func mustNoError(err error) { 12 | if err != nil { 13 | panic(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /errors_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHandleError(t *testing.T) { 11 | t.Parallel() 12 | 13 | assert.Panics(t, func() { 14 | mustNoError(errors.New("error")) 15 | }) 16 | 17 | assert.NotPanics(t, func() { 18 | mustNoError(nil) 19 | }) 20 | 21 | assert.NotPanics(t, func() { 22 | handleErr(nil) 23 | }) 24 | 25 | assert.NotPanics(t, func() { 26 | handleErr(assert.AnError) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | ) 7 | 8 | const ( 9 | metricMethodExec = "go.sql.exec" 10 | traceMethodExec = "exec" 11 | ) 12 | 13 | type execContextFuncMiddleware = middleware[execContextFunc] 14 | 15 | type execContextFunc func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) 16 | 17 | // nopExecContext executes nothing. 18 | func nopExecContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Result, error) { 19 | return nil, nil //nolint: nilnil 20 | } 21 | 22 | // skippedExecContext always returns driver.ErrSkip. 23 | func skippedExecContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Result, error) { 24 | return nil, driver.ErrSkip 25 | } 26 | 27 | // execStats records metrics for exec. 28 | func execStats(r methodRecorder, method string) execContextFuncMiddleware { 29 | return func(next execContextFunc) execContextFunc { 30 | return func(ctx context.Context, query string, args []driver.NamedValue) (result driver.Result, err error) { 31 | end := r.Record(ctx, method) 32 | 33 | defer func() { 34 | end(err) 35 | }() 36 | 37 | return next(ctx, query, args) 38 | } 39 | } 40 | } 41 | 42 | // execTrace creates a span for exec. 43 | func execTrace(t methodTracer, traceQuery queryTracer, method string) execContextFuncMiddleware { 44 | return func(next execContextFunc) execContextFunc { 45 | return func(ctx context.Context, query string, args []driver.NamedValue) (result driver.Result, err error) { 46 | ctx = ContextWithQuery(ctx, query) 47 | ctx, end := t.Trace(ctx, method) 48 | 49 | defer func() { 50 | end(err, traceQuery(ctx, query, args)...) 51 | }() 52 | 53 | return next(ctx, query, args) 54 | } 55 | } 56 | } 57 | 58 | func execWrapResult(t methodTracer, traceLastInsertID bool, traceRowsAffected bool) execContextFuncMiddleware { 59 | return func(next execContextFunc) execContextFunc { 60 | return func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 61 | result, err := next(ctx, query, args) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | shouldTrace, _ := t.ShouldTrace(ctx) 67 | 68 | return wrapResult(ctx, result, t, shouldTrace && traceLastInsertID, shouldTrace && traceRowsAffected), nil 69 | } 70 | } 71 | } 72 | 73 | func makeExecContextFuncMiddlewares(r methodRecorder, t methodTracer, cfg execConfig) []execContextFuncMiddleware { 74 | middlewares := make([]middleware[execContextFunc], 0, 3) 75 | 76 | middlewares = append(middlewares, execStats(r, cfg.metricMethod)) 77 | 78 | if t == nil { 79 | return middlewares 80 | } 81 | 82 | middlewares = append(middlewares, execTrace(t, cfg.traceQuery, cfg.traceMethod)) 83 | 84 | if cfg.traceLastInsertID || cfg.traceRowsAffected { 85 | middlewares = append(middlewares, execWrapResult(t, cfg.traceLastInsertID, cfg.traceRowsAffected)) 86 | } 87 | 88 | return middlewares 89 | } 90 | 91 | type execConfig struct { 92 | metricMethod string 93 | traceMethod string 94 | traceQuery queryTracer 95 | traceLastInsertID bool 96 | traceRowsAffected bool 97 | } 98 | 99 | func newExecConfig(opts driverOptions, metricMethod, traceMethod string) execConfig { 100 | return execConfig{ 101 | metricMethod: metricMethod, 102 | traceMethod: traceMethod, 103 | traceQuery: opts.trace.queryTracer, 104 | traceLastInsertID: opts.trace.LastInsertID, 105 | traceRowsAffected: opts.trace.RowsAffected, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.nhat.io/otelsql 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/DATA-DOG/go-sqlmock v1.5.2 9 | github.com/stretchr/testify v1.10.0 10 | github.com/swaggest/assertjson v1.9.0 11 | go.opentelemetry.io/otel v1.36.0 12 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 13 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 14 | go.opentelemetry.io/otel/metric v1.36.0 15 | go.opentelemetry.io/otel/sdk v1.36.0 16 | go.opentelemetry.io/otel/sdk/metric v1.36.0 17 | go.opentelemetry.io/otel/trace v1.36.0 18 | ) 19 | 20 | require ( 21 | github.com/bool64/shared v0.1.5 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/fsnotify/fsnotify v1.7.0 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/iancoleman/orderedmap v0.3.0 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/nxadm/tail v1.4.11 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/sergi/go-diff v1.3.1 // indirect 32 | github.com/yudai/gojsondiff v1.0.0 // indirect 33 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 34 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/sys v0.33.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /internal/test/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/swaggest/assertjson" 6 | ) 7 | 8 | // Func asserts actual input. 9 | type Func func(t assert.TestingT, actual string, msgAndArgs ...any) bool 10 | 11 | // Equal creates a new Func to check whether the two values are equal. 12 | func Equal(expect string) Func { 13 | return func(t assert.TestingT, actual string, msgAndArgs ...any) bool { 14 | return assert.Equal(t, expect, actual, msgAndArgs...) 15 | } 16 | } 17 | 18 | // EqualJSON creates a new Func to check whether the two JSON values are equal. 19 | func EqualJSON(expect string) Func { 20 | return func(t assert.TestingT, actual string, msgAndArgs ...any) bool { 21 | return assertjson.Equal(t, []byte(expect), []byte(actual), msgAndArgs...) 22 | } 23 | } 24 | 25 | // Nop creates a new Func that does not assert anything. 26 | func Nop() Func { 27 | return func(_ assert.TestingT, _ string, _ ...any) bool { 28 | return true 29 | } 30 | } 31 | 32 | // Empty creates a new Func to check whether the actual data is empty. 33 | func Empty() Func { 34 | return Equal("") 35 | } 36 | -------------------------------------------------------------------------------- /internal/test/assert/doc.go: -------------------------------------------------------------------------------- 1 | // Package assert provides asserter closures. 2 | package assert 3 | -------------------------------------------------------------------------------- /internal/test/oteltest/context.go: -------------------------------------------------------------------------------- 1 | package oteltest 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel/trace" 7 | ) 8 | 9 | // BackgroundWithSpanContext creates a new context.Background with trace id and span id. 10 | func BackgroundWithSpanContext(traceID trace.TraceID, spanID trace.SpanID) context.Context { 11 | sc := trace.NewSpanContext(trace.SpanContextConfig{ 12 | TraceID: traceID, 13 | SpanID: spanID, 14 | }) 15 | 16 | return trace.ContextWithSpanContext(context.Background(), sc) 17 | } 18 | -------------------------------------------------------------------------------- /internal/test/oteltest/doc.go: -------------------------------------------------------------------------------- 1 | // Package oteltest provides functionalities for testing otelsql. 2 | package oteltest 3 | -------------------------------------------------------------------------------- /internal/test/oteltest/metric.go: -------------------------------------------------------------------------------- 1 | package oteltest 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "go.opentelemetry.io/otel/attribute" 10 | metricsdk "go.opentelemetry.io/otel/sdk/metric" 11 | "go.opentelemetry.io/otel/sdk/metric/metricdata" 12 | ) 13 | 14 | type metricReader struct { 15 | exporter metricsdk.Exporter 16 | metricsdk.Reader 17 | } 18 | 19 | func (r *metricReader) Shutdown(ctx context.Context) error { 20 | rm := metricdata.ResourceMetrics{} 21 | if err := r.Reader.Collect(ctx, &rm); err != nil { //nolint: staticcheck 22 | return err 23 | } 24 | 25 | if err := r.exporter.Export(ctx, &rm); err != nil { 26 | return err 27 | } 28 | 29 | return r.Reader.Shutdown(ctx) 30 | } 31 | 32 | type metricData struct { 33 | Name string `json:"Name"` 34 | Last any `json:"Last,omitempty"` 35 | Sum any `json:"Sum,omitempty"` 36 | Count any `json:"Count,omitempty"` 37 | } 38 | 39 | type metricEncoder struct { 40 | *json.Encoder 41 | } 42 | 43 | func (e *metricEncoder) Encode(v any) error { 44 | resMetrics, ok := v.(*metricdata.ResourceMetrics) 45 | if !ok { 46 | return e.Encoder.Encode(v) 47 | } 48 | 49 | metrics := make([]metricData, 0) 50 | 51 | for _, scopedMetrics := range resMetrics.ScopeMetrics { 52 | for _, scopedMetric := range scopedMetrics.Metrics { 53 | attrs := resMetrics.Resource.Attributes() 54 | attrs = append(attrs, attribute.String("instrumentation.name", scopedMetrics.Scope.Name)) 55 | 56 | switch smd := scopedMetric.Data.(type) { 57 | case metricdata.Gauge[int64]: 58 | metrics = append(metrics, metricDataFromGauge(scopedMetric.Name, smd, attrs)...) 59 | 60 | case metricdata.Gauge[float64]: 61 | metrics = append(metrics, metricDataFromGauge(scopedMetric.Name, smd, attrs)...) 62 | 63 | case metricdata.Sum[int64]: 64 | metrics = append(metrics, metricDataFromSum(scopedMetric.Name, smd, attrs)...) 65 | 66 | case metricdata.Sum[float64]: 67 | metrics = append(metrics, metricDataFromSum(scopedMetric.Name, smd, attrs)...) 68 | 69 | case metricdata.Histogram[int64]: 70 | metrics = append(metrics, metricDataFromHistogram(scopedMetric.Name, smd, attrs)...) 71 | 72 | case metricdata.Histogram[float64]: 73 | metrics = append(metrics, metricDataFromHistogram(scopedMetric.Name, smd, attrs)...) 74 | } 75 | } 76 | } 77 | 78 | return e.Encoder.Encode(metrics) 79 | } 80 | 81 | func metricDataFromGauge[N int64 | float64](name string, g metricdata.Gauge[N], attrs []attribute.KeyValue) []metricData { 82 | result := make([]metricData, 0, len(g.DataPoints)) 83 | 84 | for _, dp := range g.DataPoints { 85 | result = append(result, metricData{ 86 | Name: metricDataName(name, append(attrs, dp.Attributes.ToSlice()...)), 87 | Last: dp.Value, 88 | }) 89 | } 90 | 91 | return result 92 | } 93 | 94 | func metricDataFromSum[N int64 | float64](name string, g metricdata.Sum[N], attrs []attribute.KeyValue) []metricData { 95 | result := make([]metricData, 0, len(g.DataPoints)) 96 | 97 | for _, dp := range g.DataPoints { 98 | result = append(result, metricData{ 99 | Name: metricDataName(name, append(attrs, dp.Attributes.ToSlice()...)), 100 | Sum: dp.Value, 101 | }) 102 | } 103 | 104 | return result 105 | } 106 | 107 | func metricDataFromHistogram[N int64 | float64](name string, g metricdata.Histogram[N], attrs []attribute.KeyValue) []metricData { 108 | result := make([]metricData, 0, len(g.DataPoints)) 109 | 110 | for _, dp := range g.DataPoints { 111 | result = append(result, metricData{ 112 | Name: metricDataName(name, append(attrs, dp.Attributes.ToSlice()...)), 113 | Count: dp.Count, 114 | Sum: dp.Sum, 115 | }) 116 | } 117 | 118 | return result 119 | } 120 | 121 | func metricDataName(name string, attrs []attribute.KeyValue) string { 122 | labels := make([]string, len(attrs)) 123 | 124 | for i, attr := range attrs { 125 | labels[i] = fmt.Sprintf("%s=%s", attr.Key, attr.Value.Emit()) 126 | } 127 | 128 | return fmt.Sprintf("%s{%s}", name, strings.Join(labels, ",")) 129 | } 130 | -------------------------------------------------------------------------------- /internal/test/oteltest/trace.go: -------------------------------------------------------------------------------- 1 | package oteltest 2 | 3 | import "go.opentelemetry.io/otel/trace" 4 | 5 | // NilTraceID is an empty trace id. 6 | var NilTraceID trace.TraceID 7 | 8 | // SampleTraceID is a sample of trace id. 9 | var SampleTraceID = MustParseTraceID("25239e8a2ad5562d561f2ecd6a9744de") 10 | 11 | // MustParseTraceID parse a string to trace id. 12 | func MustParseTraceID(s string) trace.TraceID { 13 | r, err := trace.TraceIDFromHex(s) 14 | handleErr(err) 15 | 16 | return r 17 | } 18 | 19 | // NilSpanID is an empty span id. 20 | var NilSpanID trace.SpanID 21 | 22 | // SampleSpanID is a sample of span id. 23 | var SampleSpanID = MustParseSpanID("1d256548fd1a0dba") 24 | 25 | // MustParseSpanID parse a string to span id. 26 | func MustParseSpanID(s string) trace.SpanID { 27 | r, err := trace.SpanIDFromHex(s) 28 | handleErr(err) 29 | 30 | return r 31 | } 32 | 33 | // Span represents a span. 34 | type Span struct { 35 | Name string `json:"Name"` 36 | SpanContext SpanContext `json:"SpanContext"` 37 | Parent SpanContext `json:"Parent"` 38 | SpanKind int `json:"SpanKind"` 39 | Attributes []SpanAttribute `json:"Attributes"` 40 | } 41 | 42 | // SpanContext represents a span context. 43 | type SpanContext struct { 44 | TraceID string `json:"TraceID"` 45 | SpanID string `json:"SpanID"` 46 | } 47 | 48 | // SpanAttribute represents a span attribute. 49 | type SpanAttribute struct { 50 | Key string `json:"Key"` 51 | Value struct { 52 | Type string `json:"Type"` 53 | Value any `json:"Value"` 54 | } `json:"Value"` 55 | } 56 | -------------------------------------------------------------------------------- /internal/test/sqlmock/connector.go: -------------------------------------------------------------------------------- 1 | package sqlmock 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | ) 9 | 10 | // DriverContext creates a new driver.DriverContext. 11 | func DriverContext(mocks ...func(Sqlmock)) driver.DriverContext { 12 | _, m, err := sqlmock.New( 13 | sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), 14 | sqlmock.MonitorPingsOption(true), 15 | ) 16 | 17 | var lazyInit driver.Driver 18 | 19 | drv := struct { 20 | driver.Driver 21 | driver.DriverContext 22 | }{ 23 | DriverContext: openConnectorFunc(func(string) (driver.Connector, error) { 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return struct { 29 | driverFunc 30 | connectFunc 31 | }{ 32 | connectFunc: func(context.Context) (driver.Conn, error) { 33 | for _, mock := range mocks { 34 | mock(m) 35 | } 36 | 37 | return m.(driver.Conn), nil //nolint: errcheck 38 | }, 39 | driverFunc: func() driver.Driver { 40 | return lazyInit 41 | }, 42 | }, nil 43 | }), 44 | } 45 | 46 | lazyInit = drv 47 | 48 | return drv 49 | } 50 | 51 | type openConnectorFunc func(name string) (driver.Connector, error) 52 | 53 | func (f openConnectorFunc) OpenConnector(name string) (driver.Connector, error) { 54 | return f(name) 55 | } 56 | 57 | type connectFunc func(context.Context) (driver.Conn, error) 58 | 59 | func (f connectFunc) Connect(ctx context.Context) (driver.Conn, error) { 60 | return f(ctx) 61 | } 62 | 63 | type driverFunc func() driver.Driver 64 | 65 | func (f driverFunc) Driver() driver.Driver { 66 | return f() 67 | } 68 | -------------------------------------------------------------------------------- /internal/test/sqlmock/doc.go: -------------------------------------------------------------------------------- 1 | // Package sqlmock provides functionalities for mocking a sql.DB connection. 2 | package sqlmock 3 | -------------------------------------------------------------------------------- /internal/test/sqlmock/sqlmock.go: -------------------------------------------------------------------------------- 1 | package sqlmock 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // Sqlmock interface. 13 | type Sqlmock = sqlmock.Sqlmock 14 | 15 | // New creates sqlmock database connection and a mock to manage expectations. 16 | var New = sqlmock.New 17 | 18 | // NewResult creates a new sql driver Result for Exec based query mocks. 19 | var NewResult = sqlmock.NewResult 20 | 21 | // NewRows allows Rows to be created from a sql driver.Value slice or from the CSV string and to be used as sql driver.Rows. 22 | var NewRows = sqlmock.NewRows 23 | 24 | // Sqlmocker mocks and returns a sqlmock instance. 25 | type Sqlmocker func(t testing.TB) string 26 | 27 | // Register creates a new sqlmock instance and returns the dsn to connect to it. 28 | func Register(mocks ...func(m Sqlmock)) Sqlmocker { 29 | return func(tb testing.TB) string { 30 | tb.Helper() 31 | 32 | mockDB, m, err := sqlmock.New( 33 | sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), 34 | sqlmock.MonitorPingsOption(true), 35 | ) 36 | require.NoError(tb, err) 37 | 38 | for _, mock := range mocks { 39 | mock(m) 40 | } 41 | 42 | tb.Cleanup(func() { 43 | assert.NoError(tb, m.ExpectationsWereMet()) 44 | 45 | // We do not care if closing mock fails. 46 | _ = mockDB.Close() // nolint: errcheck 47 | }) 48 | 49 | return getDSN(m) 50 | } 51 | } 52 | 53 | func getDSN(m Sqlmock) string { 54 | return reflect.Indirect(reflect.ValueOf(m)).FieldByName("dsn").String() 55 | } 56 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | type middleware[T any] func(next T) T 4 | 5 | // chainMiddlewares builds an inline middleware stack in the order they are passed. 6 | func chainMiddlewares[T any](middlewares []middleware[T], last T) T { 7 | // Return ahead of time if there are not any middlewares for the chain. 8 | if len(middlewares) == 0 { 9 | return last 10 | } 11 | 12 | // Wrap the end execer with the middleware chain. 13 | h := middlewares[len(middlewares)-1](last) 14 | 15 | for i := len(middlewares) - 2; i >= 0; i-- { 16 | h = middlewares[i](h) 17 | } 18 | 19 | return h 20 | } 21 | -------------------------------------------------------------------------------- /options_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "go.opentelemetry.io/otel/codes" 11 | ) 12 | 13 | func TestTraceAll(t *testing.T) { 14 | t.Parallel() 15 | 16 | o := driverOptions{} 17 | 18 | TraceAll().applyDriverOptions(&o) 19 | 20 | assert.True(t, o.trace.AllowRoot) 21 | assert.True(t, o.trace.Ping) 22 | assert.True(t, o.trace.RowsNext) 23 | assert.True(t, o.trace.RowsClose) 24 | assert.True(t, o.trace.RowsAffected) 25 | assert.True(t, o.trace.LastInsertID) 26 | 27 | var ( 28 | ctx = context.Background() 29 | query = "SELECT * FROM data WHERE country = $1" 30 | values = []driver.NamedValue{{ 31 | Ordinal: 1, 32 | Value: "US", 33 | }} 34 | ) 35 | 36 | expected := traceQueryWithArgs(ctx, query, values) 37 | actual := o.trace.queryTracer(ctx, query, values) 38 | 39 | assert.Equal(t, expected, actual) 40 | } 41 | 42 | func TestDisableErrSkip(t *testing.T) { 43 | t.Parallel() 44 | 45 | testCases := []struct { 46 | scenario string 47 | error error 48 | expectedCode codes.Code 49 | expectedDescription string 50 | }{ 51 | { 52 | scenario: "no error", 53 | error: nil, 54 | expectedCode: codes.Ok, 55 | expectedDescription: "", 56 | }, 57 | { 58 | scenario: "skip", 59 | error: driver.ErrSkip, 60 | expectedCode: codes.Ok, 61 | expectedDescription: "", 62 | }, 63 | { 64 | scenario: "no error", 65 | error: errors.New("error"), 66 | expectedCode: codes.Error, 67 | expectedDescription: "error", 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | tc := tc 73 | t.Run(tc.scenario, func(t *testing.T) { 74 | t.Parallel() 75 | 76 | o := driverOptions{} 77 | 78 | DisableErrSkip().applyDriverOptions(&o) 79 | 80 | code, description := o.trace.errorToSpanStatus(tc.error) 81 | 82 | assert.Equal(t, tc.expectedCode, code) 83 | assert.Equal(t, tc.expectedDescription, description) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ping.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | const ( 8 | metricMethodPing = "go.sql.ping" 9 | traceMethodPing = "ping" 10 | ) 11 | 12 | // pingFuncMiddleware is a type for pingFunc middleware. 13 | type pingFuncMiddleware = middleware[pingFunc] 14 | 15 | // pingFunc is a callback for pingFunc. 16 | type pingFunc func(ctx context.Context) error 17 | 18 | // nopPing pings nothing. 19 | func nopPing(_ context.Context) error { 20 | return nil 21 | } 22 | 23 | // pingStats records ping stats. 24 | func pingStats(r methodRecorder) pingFuncMiddleware { 25 | return func(next pingFunc) pingFunc { 26 | return func(ctx context.Context) (err error) { 27 | end := r.Record(ctx, metricMethodPing) 28 | 29 | defer func() { 30 | end(err) 31 | }() 32 | 33 | return next(ctx) 34 | } 35 | } 36 | } 37 | 38 | // pingTrace traces ping. 39 | func pingTrace(t methodTracer) pingFuncMiddleware { 40 | return func(next pingFunc) pingFunc { 41 | return func(ctx context.Context) (err error) { 42 | ctx, end := t.Trace(ctx, traceMethodPing) 43 | 44 | defer func() { 45 | end(err) 46 | }() 47 | 48 | return next(ctx) 49 | } 50 | } 51 | } 52 | 53 | func makePingFuncMiddlewares(r methodRecorder, t methodTracer) []pingFuncMiddleware { 54 | middlewares := make([]pingFuncMiddleware, 0, 2) 55 | middlewares = append(middlewares, pingStats(r)) 56 | 57 | if t != nil { 58 | middlewares = append(middlewares, pingTrace(t)) 59 | } 60 | 61 | return middlewares 62 | } 63 | -------------------------------------------------------------------------------- /ping_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "go.opentelemetry.io/otel/metric/noop" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 12 | 13 | "go.nhat.io/otelsql/internal/test/oteltest" 14 | ) 15 | 16 | func BenchmarkPingStats(b *testing.B) { 17 | meter := noop.NewMeterProvider().Meter("ping_test") 18 | 19 | histogram, err := meter.Float64Histogram("latency_ms") 20 | require.NoError(b, err) 21 | 22 | count, err := meter.Int64Counter("calls") 23 | require.NoError(b, err) 24 | 25 | r := newMethodRecorder(histogram.Record, count.Add, 26 | semconv.DBSystemOtherSQL, 27 | dbInstance.String("test"), 28 | ) 29 | 30 | ping := chainMiddlewares([]pingFuncMiddleware{ 31 | pingStats(r), 32 | }, nopPing) 33 | 34 | for i := 0; i < b.N; i++ { 35 | _ = ping(context.Background()) // nolint: errcheck 36 | } 37 | } 38 | 39 | func TestNopPing(t *testing.T) { 40 | t.Parallel() 41 | 42 | err := nopPing(context.Background()) 43 | 44 | assert.NoError(t, err) 45 | } 46 | 47 | func TestChainPingFuncMiddlewares_NoMiddleware(t *testing.T) { 48 | t.Parallel() 49 | 50 | f := chainMiddlewares(nil, nopPing) 51 | 52 | err := f(context.Background()) 53 | 54 | assert.NoError(t, err) 55 | } 56 | 57 | func TestChainPingFuncMiddlewares(t *testing.T) { 58 | t.Parallel() 59 | 60 | stack := make([]string, 0) 61 | 62 | pushPingFunc := func(s string) pingFunc { 63 | return func(context.Context) error { 64 | stack = append(stack, s) 65 | 66 | return nil 67 | } 68 | } 69 | 70 | pushPingFuncMiddleware := func(s string) pingFuncMiddleware { 71 | return func(next pingFunc) pingFunc { 72 | return func(ctx context.Context) error { 73 | stack = append(stack, s) 74 | 75 | return next(ctx) 76 | } 77 | } 78 | } 79 | 80 | ping := chainMiddlewares( 81 | []pingFuncMiddleware{ 82 | pushPingFuncMiddleware("outer"), 83 | pushPingFuncMiddleware("inner"), 84 | }, 85 | pushPingFunc("end"), 86 | ) 87 | err := ping(context.Background()) 88 | 89 | assert.NoError(t, err) 90 | 91 | expected := []string{"outer", "inner", "end"} 92 | 93 | assert.Equal(t, expected, stack) 94 | } 95 | 96 | func TestPingStats(t *testing.T) { 97 | t.Parallel() 98 | 99 | testCases := []struct { 100 | scenario string 101 | ping pingFunc 102 | expected string 103 | }{ 104 | { 105 | scenario: "error", 106 | ping: func(context.Context) error { 107 | return errors.New("error") 108 | }, 109 | expected: `[ 110 | { 111 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=ping_test,db.instance=test,db.operation=go.sql.ping,db.sql.error=error,db.sql.status=ERROR,db.system=other_sql}", 112 | "Sum": 1 113 | }, 114 | { 115 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=ping_test,db.instance=test,db.operation=go.sql.ping,db.sql.error=error,db.sql.status=ERROR,db.system=other_sql}", 116 | "Sum": "", 117 | "Count": 1 118 | } 119 | ]`, 120 | }, 121 | { 122 | scenario: "no error", 123 | ping: nopPing, 124 | expected: `[ 125 | { 126 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=ping_test,db.instance=test,db.operation=go.sql.ping,db.sql.status=OK,db.system=other_sql}", 127 | "Sum": 1 128 | }, 129 | { 130 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=ping_test,db.instance=test,db.operation=go.sql.ping,db.sql.status=OK,db.system=other_sql}", 131 | "Sum": "", 132 | "Count": 1 133 | } 134 | ]`, 135 | }, 136 | } 137 | 138 | for _, tc := range testCases { 139 | tc := tc 140 | t.Run(tc.scenario, func(t *testing.T) { 141 | t.Parallel() 142 | 143 | oteltest.New(oteltest.MetricsEqualJSON(tc.expected)). 144 | Run(t, func(s oteltest.SuiteContext) { 145 | meter := s.MeterProvider().Meter("ping_test") 146 | 147 | histogram, err := meter.Float64Histogram(dbSQLClientLatencyMs) 148 | require.NoError(t, err) 149 | 150 | count, err := meter.Int64Counter(dbSQLClientCalls) 151 | require.NoError(t, err) 152 | 153 | r := newMethodRecorder(histogram.Record, count.Add, 154 | semconv.DBSystemOtherSQL, 155 | dbInstance.String("test"), 156 | ) 157 | 158 | ping := chainMiddlewares([]pingFuncMiddleware{ 159 | pingStats(r), 160 | }, tc.ping) 161 | 162 | _ = ping(context.Background()) // nolint: errcheck 163 | }) 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /prepare.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | ) 7 | 8 | const ( 9 | metricMethodPrepare = "go.sql.prepare" 10 | traceMethodPrepare = "prepare" 11 | ) 12 | 13 | type prepareContextFuncMiddleware = middleware[prepareContextFunc] 14 | 15 | type prepareContextFunc func(ctx context.Context, query string) (driver.Stmt, error) 16 | 17 | // nopPrepareContext prepares nothing. 18 | func nopPrepareContext(_ context.Context, _ string) (driver.Stmt, error) { 19 | return nil, nil //nolint: nilnil 20 | } 21 | 22 | func ensurePrepareContext(conn driver.Conn) prepareContextFunc { 23 | if p, ok := conn.(driver.ConnPrepareContext); ok { 24 | return p.PrepareContext 25 | } 26 | 27 | return func(_ context.Context, query string) (driver.Stmt, error) { 28 | return conn.Prepare(query) 29 | } 30 | } 31 | 32 | // prepareStats records metrics for prepare. 33 | func prepareStats(r methodRecorder) prepareContextFuncMiddleware { 34 | return func(next prepareContextFunc) prepareContextFunc { 35 | return func(ctx context.Context, query string) (stmt driver.Stmt, err error) { 36 | end := r.Record(ctx, metricMethodPrepare) 37 | 38 | defer func() { 39 | end(err) 40 | }() 41 | 42 | return next(ctx, query) 43 | } 44 | } 45 | } 46 | 47 | // prepareTrace creates a span for prepare. 48 | func prepareTrace(t methodTracer, traceQuery queryTracer) prepareContextFuncMiddleware { 49 | return func(next prepareContextFunc) prepareContextFunc { 50 | return func(ctx context.Context, query string) (stmt driver.Stmt, err error) { 51 | ctx, end := t.Trace(ctx, traceMethodPrepare) 52 | 53 | defer func() { 54 | end(err, traceQuery(ctx, query, nil)...) 55 | }() 56 | 57 | return next(ctx, query) 58 | } 59 | } 60 | } 61 | 62 | func prepareWrapResult( 63 | execFuncMiddlewares []execContextFuncMiddleware, 64 | execContextFuncMiddlewares []execContextFuncMiddleware, 65 | queryFuncMiddlewares []queryContextFuncMiddleware, 66 | queryContextFuncMiddlewares []queryContextFuncMiddleware, 67 | ) prepareContextFuncMiddleware { 68 | return func(next prepareContextFunc) prepareContextFunc { 69 | return func(ctx context.Context, query string) (driver.Stmt, error) { 70 | stmt, err := next(ctx, query) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return wrapStmt(stmt, stmtConfig{ 76 | query: query, 77 | execFuncMiddlewares: execFuncMiddlewares, 78 | queryContextFuncMiddlewares: queryContextFuncMiddlewares, 79 | execContextFuncMiddlewares: execContextFuncMiddlewares, 80 | queryFuncMiddlewares: queryFuncMiddlewares, 81 | }), nil 82 | } 83 | } 84 | } 85 | 86 | type prepareConfig struct { 87 | traceQuery queryTracer 88 | 89 | execFuncMiddlewares []execContextFuncMiddleware 90 | execContextFuncMiddlewares []execContextFuncMiddleware 91 | queryFuncMiddlewares []queryContextFuncMiddleware 92 | queryContextFuncMiddlewares []queryContextFuncMiddleware 93 | } 94 | 95 | func makePrepareContextFuncMiddlewares(r methodRecorder, t methodTracer, cfg prepareConfig) []prepareContextFuncMiddleware { 96 | return []prepareContextFuncMiddleware{ 97 | prepareStats(r), 98 | prepareTrace(t, cfg.traceQuery), 99 | prepareWrapResult( 100 | cfg.execFuncMiddlewares, 101 | cfg.execContextFuncMiddlewares, 102 | cfg.queryFuncMiddlewares, 103 | cfg.queryContextFuncMiddlewares, 104 | ), 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | ) 7 | 8 | const ( 9 | metricMethodQuery = "go.sql.query" 10 | traceMethodQuery = "query" 11 | ) 12 | 13 | type queryContextFuncMiddleware = middleware[queryContextFunc] 14 | 15 | type queryContextFunc func(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) 16 | 17 | // nopQueryContext queries nothing. 18 | func nopQueryContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Rows, error) { 19 | return nil, nil //nolint: nilnil 20 | } 21 | 22 | // skippedQueryContext always returns driver.ErrSkip. 23 | func skippedQueryContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Rows, error) { 24 | return nil, driver.ErrSkip 25 | } 26 | 27 | // queryStats records metrics for query. 28 | func queryStats(r methodRecorder, method string) queryContextFuncMiddleware { 29 | return func(next queryContextFunc) queryContextFunc { 30 | return func(ctx context.Context, query string, args []driver.NamedValue) (result driver.Rows, err error) { 31 | end := r.Record(ctx, method) 32 | 33 | defer func() { 34 | end(err) 35 | }() 36 | 37 | result, err = next(ctx, query, args) 38 | 39 | return 40 | } 41 | } 42 | } 43 | 44 | // queryTrace creates a span for query. 45 | func queryTrace(t methodTracer, traceQuery queryTracer, method string) queryContextFuncMiddleware { 46 | return func(next queryContextFunc) queryContextFunc { 47 | return func(ctx context.Context, query string, args []driver.NamedValue) (result driver.Rows, err error) { 48 | ctx = ContextWithQuery(ctx, query) 49 | ctx, end := t.Trace(ctx, method) 50 | 51 | defer func() { 52 | end(err, traceQuery(ctx, query, args)...) 53 | }() 54 | 55 | result, err = next(ctx, query, args) 56 | 57 | return 58 | } 59 | } 60 | } 61 | 62 | func queryWrapRows(t methodTracer, traceLastInsertID bool, traceRowsAffected bool) queryContextFuncMiddleware { 63 | return func(next queryContextFunc) queryContextFunc { 64 | return func(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 65 | result, err := next(ctx, query, args) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | shouldTrace, _ := t.ShouldTrace(ctx) 71 | 72 | return wrapRows(ctx, result, t, shouldTrace && traceLastInsertID, shouldTrace && traceRowsAffected), nil 73 | } 74 | } 75 | } 76 | 77 | func makeQueryerContextMiddlewares(r methodRecorder, t methodTracer, cfg queryConfig) []queryContextFuncMiddleware { 78 | middlewares := make([]queryContextFuncMiddleware, 0, 3) 79 | 80 | middlewares = append(middlewares, queryStats(r, cfg.metricMethod)) 81 | 82 | if t == nil { 83 | return middlewares 84 | } 85 | 86 | middlewares = append(middlewares, queryTrace(t, cfg.traceQuery, cfg.traceMethod)) 87 | 88 | if cfg.traceRowsNext || cfg.traceRowsClose { 89 | middlewares = append(middlewares, queryWrapRows(t, cfg.traceRowsNext, cfg.traceRowsClose)) 90 | } 91 | 92 | return middlewares 93 | } 94 | 95 | type queryConfig struct { 96 | metricMethod string 97 | traceMethod string 98 | traceQuery queryTracer 99 | traceRowsNext bool 100 | traceRowsClose bool 101 | } 102 | 103 | func newQueryConfig(opts driverOptions, metricMethod, traceMethod string) queryConfig { 104 | return queryConfig{ 105 | metricMethod: metricMethod, 106 | traceMethod: traceMethod, 107 | traceQuery: opts.trace.queryTracer, 108 | traceRowsNext: opts.trace.RowsNext, 109 | traceRowsClose: opts.trace.RowsClose, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /recorder.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/metric" 9 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 10 | ) 11 | 12 | // float64Recorder adds a new value to the list of Histogram's records. 13 | type float64Recorder = func(ctx context.Context, value float64, opts ...metric.RecordOption) 14 | 15 | // int64Counter adds the value to the counter's sum. 16 | type int64Counter = func(ctx context.Context, value int64, opts ...metric.AddOption) 17 | 18 | // methodRecorder records metrics about a sql method. 19 | type methodRecorder interface { 20 | Record(ctx context.Context, method string, labels ...attribute.KeyValue) func(err error) 21 | } 22 | 23 | type methodRecorderImpl struct { 24 | recordLatency float64Recorder 25 | countCalls int64Counter 26 | 27 | attributes []attribute.KeyValue 28 | } 29 | 30 | func (r methodRecorderImpl) Record(ctx context.Context, method string, labels ...attribute.KeyValue) func(err error) { 31 | startTime := time.Now() 32 | 33 | attrs := make([]attribute.KeyValue, 0, len(r.attributes)+len(labels)+2) 34 | 35 | attrs = append(attrs, r.attributes...) 36 | attrs = append(attrs, labels...) 37 | attrs = append(attrs, semconv.DBOperationKey.String(method)) 38 | 39 | return func(err error) { 40 | elapsedTime := millisecondsSince(startTime) 41 | 42 | if err == nil { 43 | attrs = append(attrs, dbSQLStatusOK) 44 | } else { 45 | attrs = append(attrs, dbSQLStatusERROR, 46 | dbSQLError.String(err.Error()), 47 | ) 48 | } 49 | 50 | r.countCalls(ctx, 1, metric.WithAttributes(attrs...)) 51 | r.recordLatency(ctx, elapsedTime, metric.WithAttributes(attrs...)) 52 | } 53 | } 54 | 55 | func newMethodRecorder( 56 | latencyRecorder float64Recorder, 57 | callsCounter int64Counter, 58 | attrs ...attribute.KeyValue, 59 | ) methodRecorderImpl { 60 | return methodRecorderImpl{ 61 | recordLatency: latencyRecorder, 62 | countCalls: callsCounter, 63 | attributes: attrs, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/begin_commit_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.begin,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.commit,db.sql.status=OK}", 8 | "Sum": 1 9 | }, 10 | { 11 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.begin,db.sql.status=OK}", 12 | "Sum": "", 13 | "Count": 1 14 | }, 15 | { 16 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.commit,db.sql.status=OK}", 17 | "Sum": "", 18 | "Count": 1 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/begin_rollback_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.begin,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.rollback,db.sql.status=OK}", 8 | "Sum": 1 9 | }, 10 | { 11 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.begin,db.sql.status=OK}", 12 | "Sum": "", 13 | "Count": 1 14 | }, 15 | { 16 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.rollback,db.sql.status=OK}", 17 | "Sum": "", 18 | "Count": 1 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/custom_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.name=test,db.operation=go.sql.ping,db.sql.status=OK,db.system=postgresql}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.name=test,db.operation=go.sql.ping,db.sql.status=OK,db.system=postgresql}", 8 | "Sum": "", 9 | "Count": 1 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/exec_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.exec,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.exec,db.sql.status=OK}", 8 | "Sum": "", 9 | "Count": 1 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/ping_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.ping,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.ping,db.sql.status=OK}", 8 | "Sum": "", 9 | "Count": 1 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/prepare_context_exec_context_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.prepare,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.stmt.exec,db.sql.status=OK}", 8 | "Sum": 1 9 | }, 10 | { 11 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.prepare,db.sql.status=OK}", 12 | "Sum": "", 13 | "Count": 1 14 | }, 15 | { 16 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.stmt.exec,db.sql.status=OK}", 17 | "Sum": "", 18 | "Count": 1 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/prepare_context_query_context_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.prepare,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.stmt.query,db.sql.status=OK}", 8 | "Sum": 1 9 | }, 10 | { 11 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.prepare,db.sql.status=OK}", 12 | "Sum": "", 13 | "Count": 1 14 | }, 15 | { 16 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.stmt.query,db.sql.status=OK}", 17 | "Sum": "", 18 | "Count": 1 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/query_ok.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.query,db.sql.status=OK}", 4 | "Sum": 1 5 | }, 6 | { 7 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.operation=go.sql.query,db.sql.status=OK}", 8 | "Sum": "", 9 | "Count": 1 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /resources/fixtures/metrics/stats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "db.sql.connections.active{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 4 | "Last": 0 5 | }, 6 | { 7 | "Name": "db.sql.connections.idle_closed{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 8 | "Sum": 0 9 | }, 10 | { 11 | "Name": "db.sql.connections.idle_time_closed{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 12 | "Sum": 0 13 | }, 14 | { 15 | "Name": "db.sql.connections.idle{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 16 | "Last": 1 17 | }, 18 | { 19 | "Name": "db.sql.connections.lifetime_closed{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 20 | "Sum": 0 21 | }, 22 | { 23 | "Name": "db.sql.connections.open{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 24 | "Last": 1 25 | }, 26 | { 27 | "Name": "db.sql.connections.wait_count{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 28 | "Sum": 0 29 | }, 30 | { 31 | "Name": "db.sql.connections.wait_duration{service.name=otelsql,instrumentation.name=go.nhat.io/otelsql,db.instance=default,db.system=postgresql}", 32 | "Sum": 0 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /resources/fixtures/traces/begin_commit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:begin_transaction", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "begin_transaction" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:commit", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "commit" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/begin_commit_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:begin_transaction", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "begin_transaction" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:commit", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "commit" 87 | } 88 | } 89 | ], 90 | "Events": [ 91 | { 92 | "Name": "exception", 93 | "Attributes": [ 94 | { 95 | "Key": "exception.type", 96 | "Value": { 97 | "Type": "STRING", 98 | "Value": "go.nhat.io/otelsql_test.testError" 99 | } 100 | }, 101 | { 102 | "Key": "exception.message", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "commit error" 106 | } 107 | } 108 | ], 109 | "DroppedAttributeCount": 0, 110 | "Time": "" 111 | } 112 | ], 113 | "Links": null, 114 | "Status": { 115 | "Code": "Error", 116 | "Description": "commit error" 117 | }, 118 | "DroppedAttributes": 0, 119 | "DroppedEvents": 0, 120 | "DroppedLinks": 0, 121 | "ChildSpanCount": 0, 122 | "Resource": [ 123 | { 124 | "Key": "service.name", 125 | "Value": { 126 | "Type": "STRING", 127 | "Value": "oteltest" 128 | } 129 | } 130 | ], 131 | "InstrumentationLibrary": { 132 | "Name": "go.nhat.io/otelsql", 133 | "Version": "", 134 | "SchemaURL": "", 135 | "Attributes": null 136 | }, 137 | "InstrumentationScope": { 138 | "Name": "go.nhat.io/otelsql", 139 | "SchemaURL": "", 140 | "Version": "", 141 | "Attributes": null 142 | } 143 | } 144 | ] 145 | -------------------------------------------------------------------------------- /resources/fixtures/traces/begin_rollback.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:begin_transaction", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "begin_transaction" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:rollback", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "rollback" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/begin_rollback_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:begin_transaction", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "begin_transaction" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:rollback", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "rollback" 87 | } 88 | } 89 | ], 90 | "Events": [ 91 | { 92 | "Name": "exception", 93 | "Attributes": [ 94 | { 95 | "Key": "exception.type", 96 | "Value": { 97 | "Type": "STRING", 98 | "Value": "go.nhat.io/otelsql_test.testError" 99 | } 100 | }, 101 | { 102 | "Key": "exception.message", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "rollback error" 106 | } 107 | } 108 | ], 109 | "DroppedAttributeCount": 0, 110 | "Time": "" 111 | } 112 | ], 113 | "Links": null, 114 | "Status": { 115 | "Code": "Error", 116 | "Description": "rollback error" 117 | }, 118 | "DroppedAttributes": 0, 119 | "DroppedEvents": 0, 120 | "DroppedLinks": 0, 121 | "ChildSpanCount": 0, 122 | "Resource": [ 123 | { 124 | "Key": "service.name", 125 | "Value": { 126 | "Type": "STRING", 127 | "Value": "oteltest" 128 | } 129 | } 130 | ], 131 | "InstrumentationLibrary": { 132 | "Name": "go.nhat.io/otelsql", 133 | "Version": "", 134 | "SchemaURL": "", 135 | "Attributes": null 136 | }, 137 | "InstrumentationScope": { 138 | "Name": "go.nhat.io/otelsql", 139 | "SchemaURL": "", 140 | "Version": "", 141 | "Attributes": null 142 | } 143 | } 144 | ] 145 | -------------------------------------------------------------------------------- /resources/fixtures/traces/begin_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:begin_transaction", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "begin_transaction" 27 | } 28 | } 29 | ], 30 | "Events": [ 31 | { 32 | "Name": "exception", 33 | "Attributes": [ 34 | { 35 | "Key": "exception.type", 36 | "Value": { 37 | "Type": "STRING", 38 | "Value": "go.nhat.io/otelsql_test.testError" 39 | } 40 | }, 41 | { 42 | "Key": "exception.message", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "begin error" 46 | } 47 | } 48 | ], 49 | "DroppedAttributeCount": 0, 50 | "Time": "" 51 | } 52 | ], 53 | "Links": null, 54 | "Status": { 55 | "Code": "Error", 56 | "Description": "begin error" 57 | }, 58 | "DroppedAttributes": 0, 59 | "DroppedEvents": 0, 60 | "DroppedLinks": 0, 61 | "ChildSpanCount": 0, 62 | "Resource": [ 63 | { 64 | "Key": "service.name", 65 | "Value": { 66 | "Type": "STRING", 67 | "Value": "oteltest" 68 | } 69 | } 70 | ], 71 | "InstrumentationLibrary": { 72 | "Name": "go.nhat.io/otelsql", 73 | "Version": "", 74 | "SchemaURL": "", 75 | "Attributes": null 76 | }, 77 | "InstrumentationScope": { 78 | "Name": "go.nhat.io/otelsql", 79 | "SchemaURL": "", 80 | "Version": "", 81 | "Attributes": null 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /resources/fixtures/traces/custom.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "custom:sql:ping", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.name", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "test" 27 | } 28 | }, 29 | { 30 | "Key": "db.instance", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "default" 34 | } 35 | }, 36 | { 37 | "Key": "db.system", 38 | "Value": { 39 | "Type": "STRING", 40 | "Value": "postgresql" 41 | } 42 | }, 43 | { 44 | "Key": "db.operation", 45 | "Value": { 46 | "Type": "STRING", 47 | "Value": "ping" 48 | } 49 | } 50 | ], 51 | "Events": null, 52 | "Links": null, 53 | "Status": { 54 | "Code": "Ok", 55 | "Description": "" 56 | }, 57 | "DroppedAttributes": 0, 58 | "DroppedEvents": 0, 59 | "DroppedLinks": 0, 60 | "ChildSpanCount": 0, 61 | "Resource": [ 62 | { 63 | "Key": "service.name", 64 | "Value": { 65 | "Type": "STRING", 66 | "Value": "oteltest" 67 | } 68 | } 69 | ], 70 | "InstrumentationLibrary": { 71 | "Name": "go.nhat.io/otelsql", 72 | "Version": "", 73 | "SchemaURL": "", 74 | "Attributes": null 75 | }, 76 | "InstrumentationScope": { 77 | "Name": "go.nhat.io/otelsql", 78 | "SchemaURL": "", 79 | "Version": "", 80 | "Attributes": null 81 | } 82 | } 83 | ] 84 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_no_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_with_affected_rows.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:rows_affected", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "rows_affected" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | } 29 | ], 30 | "Events": [ 31 | { 32 | "Name": "exception", 33 | "Attributes": [ 34 | { 35 | "Key": "exception.type", 36 | "Value": { 37 | "Type": "STRING", 38 | "Value": "go.nhat.io/otelsql_test.testError" 39 | } 40 | }, 41 | { 42 | "Key": "exception.message", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "exec error" 46 | } 47 | } 48 | ], 49 | "DroppedAttributeCount": 0, 50 | "Time": "" 51 | } 52 | ], 53 | "Links": null, 54 | "Status": { 55 | "Code": "Error", 56 | "Description": "exec error" 57 | }, 58 | "DroppedAttributes": 0, 59 | "DroppedEvents": 0, 60 | "DroppedLinks": 0, 61 | "ChildSpanCount": 0, 62 | "Resource": [ 63 | { 64 | "Key": "service.name", 65 | "Value": { 66 | "Type": "STRING", 67 | "Value": "oteltest" 68 | } 69 | } 70 | ], 71 | "InstrumentationLibrary": { 72 | "Name": "go.nhat.io/otelsql", 73 | "Version": "", 74 | "SchemaURL": "", 75 | "Attributes": null 76 | }, 77 | "InstrumentationScope": { 78 | "Name": "go.nhat.io/otelsql", 79 | "SchemaURL": "", 80 | "Version": "", 81 | "Attributes": null 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_with_last_insert_id.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:last_insert_id", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "last_insert_id" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_with_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "DELETE FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /resources/fixtures/traces/exec_with_query_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:exec", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "exec" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "DELETE FROM data WHERE country = $1" 34 | } 35 | }, 36 | { 37 | "Key": "db.sql.args.1", 38 | "Value": { 39 | "Type": "STRING", 40 | "Value": "US" 41 | } 42 | } 43 | ], 44 | "Events": null, 45 | "Links": null, 46 | "Status": { 47 | "Code": "Ok", 48 | "Description": "" 49 | }, 50 | "DroppedAttributes": 0, 51 | "DroppedEvents": 0, 52 | "DroppedLinks": 0, 53 | "ChildSpanCount": 0, 54 | "Resource": [ 55 | { 56 | "Key": "service.name", 57 | "Value": { 58 | "Type": "STRING", 59 | "Value": "oteltest" 60 | } 61 | } 62 | ], 63 | "InstrumentationLibrary": { 64 | "Name": "go.nhat.io/otelsql", 65 | "Version": "", 66 | "SchemaURL": "", 67 | "Attributes": null 68 | }, 69 | "InstrumentationScope": { 70 | "Name": "go.nhat.io/otelsql", 71 | "SchemaURL": "", 72 | "Version": "", 73 | "Attributes": null 74 | } 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /resources/fixtures/traces/ping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:ping", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "ping" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /resources/fixtures/traces/ping_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:ping", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "ping" 27 | } 28 | } 29 | ], 30 | "Events": [ 31 | { 32 | "Name": "exception", 33 | "Attributes": [ 34 | { 35 | "Key": "exception.type", 36 | "Value": { 37 | "Type": "STRING", 38 | "Value": "go.nhat.io/otelsql_test.testError" 39 | } 40 | }, 41 | { 42 | "Key": "exception.message", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "ping error" 46 | } 47 | } 48 | ], 49 | "DroppedAttributeCount": 0, 50 | "Time": "" 51 | } 52 | ], 53 | "Links": null, 54 | "Status": { 55 | "Code": "Error", 56 | "Description": "ping error" 57 | }, 58 | "DroppedAttributes": 0, 59 | "DroppedEvents": 0, 60 | "DroppedLinks": 0, 61 | "ChildSpanCount": 0, 62 | "Resource": [ 63 | { 64 | "Key": "service.name", 65 | "Value": { 66 | "Type": "STRING", 67 | "Value": "oteltest" 68 | } 69 | } 70 | ], 71 | "InstrumentationLibrary": { 72 | "Name": "go.nhat.io/otelsql", 73 | "Version": "", 74 | "SchemaURL": "", 75 | "Attributes": null 76 | }, 77 | "InstrumentationScope": { 78 | "Name": "go.nhat.io/otelsql", 79 | "SchemaURL": "", 80 | "Version": "", 81 | "Attributes": null 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_exec_context_no_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:exec", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "exec" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_exec_context_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:exec", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "exec" 87 | } 88 | } 89 | ], 90 | "Events": [ 91 | { 92 | "Name": "exception", 93 | "Attributes": [ 94 | { 95 | "Key": "exception.type", 96 | "Value": { 97 | "Type": "STRING", 98 | "Value": "go.nhat.io/otelsql_test.testError" 99 | } 100 | }, 101 | { 102 | "Key": "exception.message", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "exec error" 106 | } 107 | } 108 | ], 109 | "DroppedAttributeCount": 0, 110 | "Time": "" 111 | } 112 | ], 113 | "Links": null, 114 | "Status": { 115 | "Code": "Error", 116 | "Description": "exec error" 117 | }, 118 | "DroppedAttributes": 0, 119 | "DroppedEvents": 0, 120 | "DroppedLinks": 0, 121 | "ChildSpanCount": 0, 122 | "Resource": [ 123 | { 124 | "Key": "service.name", 125 | "Value": { 126 | "Type": "STRING", 127 | "Value": "oteltest" 128 | } 129 | } 130 | ], 131 | "InstrumentationLibrary": { 132 | "Name": "go.nhat.io/otelsql", 133 | "Version": "", 134 | "SchemaURL": "", 135 | "Attributes": null 136 | }, 137 | "InstrumentationScope": { 138 | "Name": "go.nhat.io/otelsql", 139 | "SchemaURL": "", 140 | "Version": "", 141 | "Attributes": null 142 | } 143 | } 144 | ] 145 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_exec_context_with_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "DELETE FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | }, 69 | { 70 | "Name": "sql:exec", 71 | "SpanContext": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "01", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "Parent": { 79 | "TraceID": "", 80 | "SpanID": "", 81 | "TraceFlags": "", 82 | "TraceState": "", 83 | "Remote": false 84 | }, 85 | "SpanKind": 3, 86 | "StartTime": "", 87 | "EndTime": "", 88 | "Attributes": [ 89 | { 90 | "Key": "db.operation", 91 | "Value": { 92 | "Type": "STRING", 93 | "Value": "exec" 94 | } 95 | }, 96 | { 97 | "Key": "db.statement", 98 | "Value": { 99 | "Type": "STRING", 100 | "Value": "DELETE FROM data WHERE country = $1" 101 | } 102 | } 103 | ], 104 | "Events": null, 105 | "Links": null, 106 | "Status": { 107 | "Code": "Ok", 108 | "Description": "" 109 | }, 110 | "DroppedAttributes": 0, 111 | "DroppedEvents": 0, 112 | "DroppedLinks": 0, 113 | "ChildSpanCount": 0, 114 | "Resource": [ 115 | { 116 | "Key": "service.name", 117 | "Value": { 118 | "Type": "STRING", 119 | "Value": "oteltest" 120 | } 121 | } 122 | ], 123 | "InstrumentationLibrary": { 124 | "Name": "go.nhat.io/otelsql", 125 | "Version": "", 126 | "SchemaURL": "", 127 | "Attributes": null 128 | }, 129 | "InstrumentationScope": { 130 | "Name": "go.nhat.io/otelsql", 131 | "SchemaURL": "", 132 | "Version": "", 133 | "Attributes": null 134 | } 135 | } 136 | ] 137 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_exec_context_with_query_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "DELETE FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | }, 69 | { 70 | "Name": "sql:exec", 71 | "SpanContext": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "01", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "Parent": { 79 | "TraceID": "", 80 | "SpanID": "", 81 | "TraceFlags": "", 82 | "TraceState": "", 83 | "Remote": false 84 | }, 85 | "SpanKind": 3, 86 | "StartTime": "", 87 | "EndTime": "", 88 | "Attributes": [ 89 | { 90 | "Key": "db.operation", 91 | "Value": { 92 | "Type": "STRING", 93 | "Value": "exec" 94 | } 95 | }, 96 | { 97 | "Key": "db.statement", 98 | "Value": { 99 | "Type": "STRING", 100 | "Value": "DELETE FROM data WHERE country = $1" 101 | } 102 | }, 103 | { 104 | "Key": "db.sql.args.1", 105 | "Value": { 106 | "Type": "STRING", 107 | "Value": "US" 108 | } 109 | } 110 | ], 111 | "Events": null, 112 | "Links": null, 113 | "Status": { 114 | "Code": "Ok", 115 | "Description": "" 116 | }, 117 | "DroppedAttributes": 0, 118 | "DroppedEvents": 0, 119 | "DroppedLinks": 0, 120 | "ChildSpanCount": 0, 121 | "Resource": [ 122 | { 123 | "Key": "service.name", 124 | "Value": { 125 | "Type": "STRING", 126 | "Value": "oteltest" 127 | } 128 | } 129 | ], 130 | "InstrumentationLibrary": { 131 | "Name": "go.nhat.io/otelsql", 132 | "Version": "", 133 | "SchemaURL": "", 134 | "Attributes": null 135 | }, 136 | "InstrumentationScope": { 137 | "Name": "go.nhat.io/otelsql", 138 | "SchemaURL": "", 139 | "Version": "", 140 | "Attributes": null 141 | } 142 | } 143 | ] 144 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_query_context_no_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:query", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "query" 87 | } 88 | } 89 | ], 90 | "Events": null, 91 | "Links": null, 92 | "Status": { 93 | "Code": "Ok", 94 | "Description": "" 95 | }, 96 | "DroppedAttributes": 0, 97 | "DroppedEvents": 0, 98 | "DroppedLinks": 0, 99 | "ChildSpanCount": 0, 100 | "Resource": [ 101 | { 102 | "Key": "service.name", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "oteltest" 106 | } 107 | } 108 | ], 109 | "InstrumentationLibrary": { 110 | "Name": "go.nhat.io/otelsql", 111 | "Version": "", 112 | "SchemaURL": "", 113 | "Attributes": null 114 | }, 115 | "InstrumentationScope": { 116 | "Name": "go.nhat.io/otelsql", 117 | "SchemaURL": "", 118 | "Version": "", 119 | "Attributes": null 120 | } 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_query_context_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:query", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "query" 87 | } 88 | } 89 | ], 90 | "Events": [ 91 | { 92 | "Name": "exception", 93 | "Attributes": [ 94 | { 95 | "Key": "exception.type", 96 | "Value": { 97 | "Type": "STRING", 98 | "Value": "go.nhat.io/otelsql_test.testError" 99 | } 100 | }, 101 | { 102 | "Key": "exception.message", 103 | "Value": { 104 | "Type": "STRING", 105 | "Value": "query error" 106 | } 107 | } 108 | ], 109 | "DroppedAttributeCount": 0, 110 | "Time": "" 111 | } 112 | ], 113 | "Links": null, 114 | "Status": { 115 | "Code": "Error", 116 | "Description": "query error" 117 | }, 118 | "DroppedAttributes": 0, 119 | "DroppedEvents": 0, 120 | "DroppedLinks": 0, 121 | "ChildSpanCount": 0, 122 | "Resource": [ 123 | { 124 | "Key": "service.name", 125 | "Value": { 126 | "Type": "STRING", 127 | "Value": "oteltest" 128 | } 129 | } 130 | ], 131 | "InstrumentationLibrary": { 132 | "Name": "go.nhat.io/otelsql", 133 | "Version": "", 134 | "SchemaURL": "", 135 | "Attributes": null 136 | }, 137 | "InstrumentationScope": { 138 | "Name": "go.nhat.io/otelsql", 139 | "SchemaURL": "", 140 | "Version": "", 141 | "Attributes": null 142 | } 143 | } 144 | ] 145 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_query_context_with_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "SELECT * FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | }, 69 | { 70 | "Name": "sql:query", 71 | "SpanContext": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "01", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "Parent": { 79 | "TraceID": "", 80 | "SpanID": "", 81 | "TraceFlags": "", 82 | "TraceState": "", 83 | "Remote": false 84 | }, 85 | "SpanKind": 3, 86 | "StartTime": "", 87 | "EndTime": "", 88 | "Attributes": [ 89 | { 90 | "Key": "db.operation", 91 | "Value": { 92 | "Type": "STRING", 93 | "Value": "query" 94 | } 95 | }, 96 | { 97 | "Key": "db.statement", 98 | "Value": { 99 | "Type": "STRING", 100 | "Value": "SELECT * FROM data WHERE country = $1" 101 | } 102 | } 103 | ], 104 | "Events": null, 105 | "Links": null, 106 | "Status": { 107 | "Code": "Ok", 108 | "Description": "" 109 | }, 110 | "DroppedAttributes": 0, 111 | "DroppedEvents": 0, 112 | "DroppedLinks": 0, 113 | "ChildSpanCount": 0, 114 | "Resource": [ 115 | { 116 | "Key": "service.name", 117 | "Value": { 118 | "Type": "STRING", 119 | "Value": "oteltest" 120 | } 121 | } 122 | ], 123 | "InstrumentationLibrary": { 124 | "Name": "go.nhat.io/otelsql", 125 | "Version": "", 126 | "SchemaURL": "", 127 | "Attributes": null 128 | }, 129 | "InstrumentationScope": { 130 | "Name": "go.nhat.io/otelsql", 131 | "SchemaURL": "", 132 | "Version": "", 133 | "Attributes": null 134 | } 135 | } 136 | ] 137 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_query_context_with_query_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "SELECT * FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | }, 69 | { 70 | "Name": "sql:query", 71 | "SpanContext": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "01", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "Parent": { 79 | "TraceID": "", 80 | "SpanID": "", 81 | "TraceFlags": "", 82 | "TraceState": "", 83 | "Remote": false 84 | }, 85 | "SpanKind": 3, 86 | "StartTime": "", 87 | "EndTime": "", 88 | "Attributes": [ 89 | { 90 | "Key": "db.operation", 91 | "Value": { 92 | "Type": "STRING", 93 | "Value": "query" 94 | } 95 | }, 96 | { 97 | "Key": "db.statement", 98 | "Value": { 99 | "Type": "STRING", 100 | "Value": "SELECT * FROM data WHERE country = $1" 101 | } 102 | }, 103 | { 104 | "Key": "db.sql.args.1", 105 | "Value": { 106 | "Type": "STRING", 107 | "Value": "US" 108 | } 109 | } 110 | ], 111 | "Events": null, 112 | "Links": null, 113 | "Status": { 114 | "Code": "Ok", 115 | "Description": "" 116 | }, 117 | "DroppedAttributes": 0, 118 | "DroppedEvents": 0, 119 | "DroppedLinks": 0, 120 | "ChildSpanCount": 0, 121 | "Resource": [ 122 | { 123 | "Key": "service.name", 124 | "Value": { 125 | "Type": "STRING", 126 | "Value": "oteltest" 127 | } 128 | } 129 | ], 130 | "InstrumentationLibrary": { 131 | "Name": "go.nhat.io/otelsql", 132 | "Version": "", 133 | "SchemaURL": "", 134 | "Attributes": null 135 | }, 136 | "InstrumentationScope": { 137 | "Name": "go.nhat.io/otelsql", 138 | "SchemaURL": "", 139 | "Version": "", 140 | "Attributes": null 141 | } 142 | } 143 | ] 144 | -------------------------------------------------------------------------------- /resources/fixtures/traces/prepare_context_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:prepare", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "prepare" 27 | } 28 | } 29 | ], 30 | "Events": [ 31 | { 32 | "Name": "exception", 33 | "Attributes": [ 34 | { 35 | "Key": "exception.type", 36 | "Value": { 37 | "Type": "STRING", 38 | "Value": "go.nhat.io/otelsql_test.testError" 39 | } 40 | }, 41 | { 42 | "Key": "exception.message", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "prepare error" 46 | } 47 | } 48 | ], 49 | "DroppedAttributeCount": 0, 50 | "Time": "" 51 | } 52 | ], 53 | "Links": null, 54 | "Status": { 55 | "Code": "Error", 56 | "Description": "prepare error" 57 | }, 58 | "DroppedAttributes": 0, 59 | "DroppedEvents": 0, 60 | "DroppedLinks": 0, 61 | "ChildSpanCount": 0, 62 | "Resource": [ 63 | { 64 | "Key": "service.name", 65 | "Value": { 66 | "Type": "STRING", 67 | "Value": "oteltest" 68 | } 69 | } 70 | ], 71 | "InstrumentationLibrary": { 72 | "Name": "go.nhat.io/otelsql", 73 | "Version": "", 74 | "SchemaURL": "", 75 | "Attributes": null 76 | }, 77 | "InstrumentationScope": { 78 | "Name": "go.nhat.io/otelsql", 79 | "SchemaURL": "", 80 | "Version": "", 81 | "Attributes": null 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /resources/fixtures/traces/query_no_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:query", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "query" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /resources/fixtures/traces/query_with_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:query", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "query" 27 | } 28 | } 29 | ], 30 | "Events": [ 31 | { 32 | "Name": "exception", 33 | "Attributes": [ 34 | { 35 | "Key": "exception.type", 36 | "Value": { 37 | "Type": "STRING", 38 | "Value": "go.nhat.io/otelsql_test.testError" 39 | } 40 | }, 41 | { 42 | "Key": "exception.message", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "query error" 46 | } 47 | } 48 | ], 49 | "DroppedAttributeCount": 0, 50 | "Time": "" 51 | } 52 | ], 53 | "Links": null, 54 | "Status": { 55 | "Code": "Error", 56 | "Description": "query error" 57 | }, 58 | "DroppedAttributes": 0, 59 | "DroppedEvents": 0, 60 | "DroppedLinks": 0, 61 | "ChildSpanCount": 0, 62 | "Resource": [ 63 | { 64 | "Key": "service.name", 65 | "Value": { 66 | "Type": "STRING", 67 | "Value": "oteltest" 68 | } 69 | } 70 | ], 71 | "InstrumentationLibrary": { 72 | "Name": "go.nhat.io/otelsql", 73 | "Version": "", 74 | "SchemaURL": "", 75 | "Attributes": null 76 | }, 77 | "InstrumentationScope": { 78 | "Name": "go.nhat.io/otelsql", 79 | "SchemaURL": "", 80 | "Version": "", 81 | "Attributes": null 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /resources/fixtures/traces/query_with_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:query", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "query" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "SELECT * FROM data WHERE country = $1" 34 | } 35 | } 36 | ], 37 | "Events": null, 38 | "Links": null, 39 | "Status": { 40 | "Code": "Ok", 41 | "Description": "" 42 | }, 43 | "DroppedAttributes": 0, 44 | "DroppedEvents": 0, 45 | "DroppedLinks": 0, 46 | "ChildSpanCount": 0, 47 | "Resource": [ 48 | { 49 | "Key": "service.name", 50 | "Value": { 51 | "Type": "STRING", 52 | "Value": "oteltest" 53 | } 54 | } 55 | ], 56 | "InstrumentationLibrary": { 57 | "Name": "go.nhat.io/otelsql", 58 | "Version": "", 59 | "SchemaURL": "", 60 | "Attributes": null 61 | }, 62 | "InstrumentationScope": { 63 | "Name": "go.nhat.io/otelsql", 64 | "SchemaURL": "", 65 | "Version": "", 66 | "Attributes": null 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /resources/fixtures/traces/query_with_query_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:query", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "query" 27 | } 28 | }, 29 | { 30 | "Key": "db.statement", 31 | "Value": { 32 | "Type": "STRING", 33 | "Value": "SELECT * FROM data WHERE country = $1" 34 | } 35 | }, 36 | { 37 | "Key": "db.sql.args.1", 38 | "Value": { 39 | "Type": "STRING", 40 | "Value": "US" 41 | } 42 | } 43 | ], 44 | "Events": null, 45 | "Links": null, 46 | "Status": { 47 | "Code": "Ok", 48 | "Description": "" 49 | }, 50 | "DroppedAttributes": 0, 51 | "DroppedEvents": 0, 52 | "DroppedLinks": 0, 53 | "ChildSpanCount": 0, 54 | "Resource": [ 55 | { 56 | "Key": "service.name", 57 | "Value": { 58 | "Type": "STRING", 59 | "Value": "oteltest" 60 | } 61 | } 62 | ], 63 | "InstrumentationLibrary": { 64 | "Name": "go.nhat.io/otelsql", 65 | "Version": "", 66 | "SchemaURL": "", 67 | "Attributes": null 68 | }, 69 | "InstrumentationScope": { 70 | "Name": "go.nhat.io/otelsql", 71 | "SchemaURL": "", 72 | "Version": "", 73 | "Attributes": null 74 | } 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /resources/fixtures/traces/query_with_rows_close.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "sql:query", 4 | "SpanContext": { 5 | "TraceID": "", 6 | "SpanID": "", 7 | "TraceFlags": "01", 8 | "TraceState": "", 9 | "Remote": false 10 | }, 11 | "Parent": { 12 | "TraceID": "%s", 13 | "SpanID": "%s", 14 | "TraceFlags": "00", 15 | "TraceState": "", 16 | "Remote": false 17 | }, 18 | "SpanKind": 3, 19 | "StartTime": "", 20 | "EndTime": "", 21 | "Attributes": [ 22 | { 23 | "Key": "db.operation", 24 | "Value": { 25 | "Type": "STRING", 26 | "Value": "query" 27 | } 28 | } 29 | ], 30 | "Events": null, 31 | "Links": null, 32 | "Status": { 33 | "Code": "Ok", 34 | "Description": "" 35 | }, 36 | "DroppedAttributes": 0, 37 | "DroppedEvents": 0, 38 | "DroppedLinks": 0, 39 | "ChildSpanCount": 0, 40 | "Resource": [ 41 | { 42 | "Key": "service.name", 43 | "Value": { 44 | "Type": "STRING", 45 | "Value": "oteltest" 46 | } 47 | } 48 | ], 49 | "InstrumentationLibrary": { 50 | "Name": "go.nhat.io/otelsql", 51 | "Version": "", 52 | "SchemaURL": "", 53 | "Attributes": null 54 | }, 55 | "InstrumentationScope": { 56 | "Name": "go.nhat.io/otelsql", 57 | "SchemaURL": "", 58 | "Version": "", 59 | "Attributes": null 60 | } 61 | }, 62 | { 63 | "Name": "sql:rows_close", 64 | "SpanContext": { 65 | "TraceID": "", 66 | "SpanID": "", 67 | "TraceFlags": "01", 68 | "TraceState": "", 69 | "Remote": false 70 | }, 71 | "Parent": { 72 | "TraceID": "", 73 | "SpanID": "", 74 | "TraceFlags": "", 75 | "TraceState": "", 76 | "Remote": false 77 | }, 78 | "SpanKind": 3, 79 | "StartTime": "", 80 | "EndTime": "", 81 | "Attributes": [ 82 | { 83 | "Key": "db.operation", 84 | "Value": { 85 | "Type": "STRING", 86 | "Value": "rows_close" 87 | } 88 | }, 89 | { 90 | "Key": "db.sql.rows_next.success_count", 91 | "Value": { 92 | "Type": "INT64", 93 | "Value": "" 94 | } 95 | }, 96 | { 97 | "Key": "db.sql.rows_next.latency_avg", 98 | "Value": { 99 | "Type": "STRING", 100 | "Value": "" 101 | } 102 | } 103 | ], 104 | "Events": null, 105 | "Links": null, 106 | "Status": { 107 | "Code": "Ok", 108 | "Description": "" 109 | }, 110 | "DroppedAttributes": 0, 111 | "DroppedEvents": 0, 112 | "DroppedLinks": 0, 113 | "ChildSpanCount": 0, 114 | "Resource": [ 115 | { 116 | "Key": "service.name", 117 | "Value": { 118 | "Type": "STRING", 119 | "Value": "oteltest" 120 | } 121 | } 122 | ], 123 | "InstrumentationLibrary": { 124 | "Name": "go.nhat.io/otelsql", 125 | "Version": "", 126 | "SchemaURL": "", 127 | "Attributes": null 128 | }, 129 | "InstrumentationScope": { 130 | "Name": "go.nhat.io/otelsql", 131 | "SchemaURL": "", 132 | "Version": "", 133 | "Attributes": null 134 | } 135 | } 136 | ] 137 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | ) 9 | 10 | const ( 11 | traceMethodLastInsertID = "last_insert_id" 12 | traceMethodRowsAffected = "rows_affected" 13 | ) 14 | 15 | type resultFunc func() (int64, error) 16 | 17 | var _ driver.Result = (*result)(nil) 18 | 19 | type result struct { 20 | lastInsertIDFunc resultFunc 21 | rowsAffectedFunc resultFunc 22 | } 23 | 24 | func (r result) LastInsertId() (int64, error) { 25 | return r.lastInsertIDFunc() 26 | } 27 | 28 | func (r result) RowsAffected() (int64, error) { 29 | return r.rowsAffectedFunc() 30 | } 31 | 32 | func wrapResult(ctx context.Context, parent driver.Result, t methodTracer, traceLastInsertID bool, traceRowsAffected bool) driver.Result { 33 | if !traceLastInsertID && !traceRowsAffected { 34 | return parent 35 | } 36 | 37 | ctx = trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(ctx)) 38 | 39 | r := &result{ 40 | lastInsertIDFunc: parent.LastInsertId, 41 | rowsAffectedFunc: parent.RowsAffected, 42 | } 43 | 44 | if traceLastInsertID { 45 | r.lastInsertIDFunc = resultTrace(ctx, t, traceMethodLastInsertID, parent.LastInsertId) 46 | } 47 | 48 | if traceRowsAffected { 49 | r.rowsAffectedFunc = resultTrace(ctx, t, traceMethodRowsAffected, parent.RowsAffected) 50 | } 51 | 52 | return r 53 | } 54 | 55 | func resultTrace(ctx context.Context, t methodTracer, method string, f resultFunc) resultFunc { 56 | return func() (result int64, err error) { 57 | _, end := t.MustTrace(ctx, method) 58 | 59 | defer func() { 60 | end(err) 61 | }() 62 | 63 | result, err = f() 64 | 65 | return 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | package otelsql_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 9 | 10 | "go.nhat.io/otelsql" 11 | "go.nhat.io/otelsql/internal/test/oteltest" 12 | "go.nhat.io/otelsql/internal/test/sqlmock" 13 | ) 14 | 15 | func TestRecordStats(t *testing.T) { 16 | t.Parallel() 17 | 18 | expectedMetrics := expectedStatsMetric() 19 | 20 | oteltest.New( 21 | oteltest.MetricsEqualJSON(expectedMetrics), 22 | oteltest.MockDatabase(func(m sqlmock.Sqlmock) { 23 | m.ExpectPing() 24 | }), 25 | ). 26 | Run(t, func(sc oteltest.SuiteContext) { 27 | db, err := newDB(sc.DatabaseDSN()) 28 | require.NoError(t, err) 29 | 30 | err = otelsql.RecordStats(db, 31 | otelsql.WithMeterProvider(sc.MeterProvider()), 32 | otelsql.WithMinimumReadDBStatsInterval(100*time.Millisecond), 33 | otelsql.WithInstanceName("default"), 34 | otelsql.WithSystem(semconv.DBSystemPostgreSQL), 35 | ) 36 | require.NoError(t, err) 37 | 38 | err = db.Ping() 39 | require.NoError(t, err) 40 | }) 41 | } 42 | 43 | func expectedStatsMetric() string { 44 | return expectedMetricsFromFile("stats.json") 45 | } 46 | -------------------------------------------------------------------------------- /tests/.gherkin-lintrc: -------------------------------------------------------------------------------- 1 | { 2 | "file-name": [ 3 | "on", 4 | { 5 | "style": "PascalCase" 6 | } 7 | ], 8 | "no-files-without-scenarios": "on", 9 | "no-unnamed-features": "on", 10 | "no-unnamed-scenarios": "on", 11 | "no-dupe-scenario-names": [ 12 | "on", 13 | "in-feature" 14 | ], 15 | "no-dupe-feature-names": "on", 16 | "no-partially-commented-tag-lines": "on", 17 | "indentation": [ 18 | "on", 19 | { 20 | "Feature": 0, 21 | "Background": 4, 22 | "Scenario": 4, 23 | "Step": 8, 24 | "Examples": 8, 25 | "example": 12, 26 | "given": 8, 27 | "when": 8, 28 | "then": 8, 29 | "and": 8, 30 | "but": 8, 31 | "feature tag": 0, 32 | "scenario tag": 4 33 | } 34 | ], 35 | "no-trailing-spaces": "on", 36 | "new-line-at-eof": [ 37 | "on", 38 | "yes" 39 | ], 40 | "no-multiple-empty-lines": "on", 41 | "no-empty-file": "on", 42 | "no-scenario-outlines-without-examples": "on", 43 | "name-length": [ 44 | "on", 45 | { 46 | "Feature": 120, 47 | "Scenario": 120, 48 | "Step": 120 49 | } 50 | ], 51 | "no-restricted-tags": [ 52 | "on", 53 | { 54 | "tags": [ 55 | "@dev", 56 | "@watch", 57 | "@wip" 58 | ] 59 | } 60 | ], 61 | "use-and": "on", 62 | "no-duplicate-tags": "on", 63 | "no-superfluous-tags": "on", 64 | "no-homogenous-tags": "on", 65 | "one-space-between-tags": "on", 66 | "no-unused-variables": "on", 67 | "no-background-only-scenario": "on", 68 | "no-empty-background": "on", 69 | "scenario-size": [ 70 | "on", 71 | { 72 | "steps-length": { 73 | "Background": 30, 74 | "Scenario": 30 75 | } 76 | } 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /tests/mssql/doc.go: -------------------------------------------------------------------------------- 1 | // Package mssql provides compatibility tests for mssql drivers. 2 | package mssql 3 | -------------------------------------------------------------------------------- /tests/mssql/main_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/Masterminds/squirrel" 8 | "go.nhat.io/testcontainers-extra" 9 | "go.nhat.io/testcontainers-registry/mssql" 10 | 11 | "go.nhat.io/otelsql/tests/suite" 12 | ) 13 | 14 | const ( 15 | defaultVersion = "2019-latest" 16 | 17 | databaseName = "otelsql" 18 | databasePassword = "OneWrapperToTraceThemAll!" 19 | ) 20 | 21 | func TestIntegration(t *testing.T) { 22 | suite.Run(t, 23 | suite.WithTestContainerRequests( 24 | mssql.Request(databaseName, databasePassword, 25 | mssql.RunMigrations("file://./resources/migrations/"), 26 | testcontainers.WithImageTag(imageTag()), 27 | ), 28 | ), 29 | suite.WithDatabaseDriver("sqlserver"), 30 | suite.WithDatabaseDSN(mssql.DSN(databaseName, databasePassword)), 31 | suite.WithDatabasePlaceholderFormat(squirrel.AtP), 32 | suite.WithFeatureFilesLocation("../features"), 33 | suite.WithCustomerRepositoryConstructor(newRepository()), 34 | ) 35 | } 36 | 37 | func imageTag() string { 38 | v := os.Getenv("MSSQL_VERSION") 39 | if v == "" { 40 | return defaultVersion 41 | } 42 | 43 | return v 44 | } 45 | -------------------------------------------------------------------------------- /tests/mssql/repository.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "context" 5 | 6 | "go.nhat.io/clock" 7 | 8 | "go.nhat.io/otelsql/tests/suite" 9 | "go.nhat.io/otelsql/tests/suite/customer" 10 | ) 11 | 12 | type repository struct { 13 | db suite.DatabaseExecer 14 | clock clock.Clock 15 | } 16 | 17 | func (r *repository) Find(ctx context.Context, id int) (*customer.Customer, error) { 18 | row, err := r.db.QueryRow(ctx, 19 | "SELECT TOP 1 * FROM customer WHERE id = @p1", 20 | id, 21 | ) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | c := &customer.Customer{} 27 | 28 | if err := row.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 29 | return nil, err 30 | } 31 | 32 | return c, nil 33 | } 34 | 35 | func (r *repository) FindAll(ctx context.Context) ([]customer.Customer, error) { 36 | rows, err := r.db.Query(ctx, "SELECT * FROM customer") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defer func() { 42 | _ = rows.Close() // nolint: errcheck 43 | _ = rows.Err() // nolint: errcheck 44 | }() 45 | 46 | customers := make([]customer.Customer, 0) 47 | 48 | for rows.Next() { 49 | c := customer.Customer{} 50 | 51 | if err := rows.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 52 | return nil, err 53 | } 54 | 55 | customers = append(customers, c) 56 | } 57 | 58 | return customers, nil 59 | } 60 | 61 | func (r *repository) Create(ctx context.Context, user customer.Customer) error { 62 | _, err := r.db.Exec(ctx, 63 | "INSERT INTO customer VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7)", 64 | user.ID, 65 | user.Country, 66 | user.FirstName, 67 | user.LastName, 68 | user.Email, 69 | user.CreatedAt, 70 | user.UpdatedAt, 71 | ) 72 | 73 | return err 74 | } 75 | 76 | func (r *repository) Update(ctx context.Context, user customer.Customer) error { 77 | _, err := r.db.Exec(ctx, 78 | "UPDATE customer SET country = @p1, first_name = @p2, last_name = @p3, email = @p4, created_at = @p5, updated_at = @p6 WHERE id = @p7", 79 | user.Country, 80 | user.FirstName, 81 | user.LastName, 82 | user.Email, 83 | user.CreatedAt, 84 | r.clock.Now(), 85 | user.ID, 86 | ) 87 | 88 | return err 89 | } 90 | 91 | func (r *repository) Delete(ctx context.Context, id int) error { 92 | _, err := r.db.Exec(ctx, 93 | "DELETE FROM customer WHERE id = @p1", 94 | id, 95 | ) 96 | 97 | return err 98 | } 99 | 100 | func newRepository() suite.CustomerRepositoryConstructor { 101 | return func(db suite.DatabaseExecer, c clock.Clock) customer.Repository { 102 | return &repository{ 103 | db: db, 104 | clock: c, 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/mssql/resources/migrations/1_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE customer ( 2 | id INT NOT NULL PRIMARY KEY, 3 | country VARCHAR(2) NOT NULL, 4 | first_name VARCHAR(50) NOT NULL, 5 | last_name VARCHAR(50) NOT NULL, 6 | email VARCHAR(200) NOT NULL, 7 | created_at DATETIMEOFFSET NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | updated_at DATETIMEOFFSET 9 | ); 10 | 11 | CREATE INDEX customer_country ON customer(country); 12 | CREATE UNIQUE INDEX customer_email ON customer(email); 13 | -------------------------------------------------------------------------------- /tests/mysql/doc.go: -------------------------------------------------------------------------------- 1 | // Package mysql provides compatibility tests for mysql driver. 2 | package mysql 3 | -------------------------------------------------------------------------------- /tests/mysql/main_test.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/Masterminds/squirrel" 8 | _ "github.com/go-sql-driver/mysql" // Database driver 9 | "go.nhat.io/testcontainers-extra" 10 | "go.nhat.io/testcontainers-registry/mysql" 11 | 12 | "go.nhat.io/otelsql/tests/suite" 13 | ) 14 | 15 | const ( 16 | defaultVersion = "8" 17 | defaultImage = "mysql" 18 | defaultDriver = "mysql" 19 | 20 | databaseName = "otelsql" 21 | databaseUsername = "otelsql" 22 | databasePassword = "OneWrapperToTraceThemAll" 23 | ) 24 | 25 | func TestIntegration(t *testing.T) { 26 | suite.Run(t, 27 | suite.WithTestContainerRequests( 28 | mysql.Request(databaseName, databaseUsername, databasePassword, 29 | mysql.RunMigrations("file://./resources/migrations/"), 30 | testcontainers.WithImageName(imageName()), 31 | testcontainers.WithImageTag(imageTag()), 32 | ), 33 | ), 34 | suite.WithDatabaseDriver(defaultDriver), 35 | suite.WithDatabaseDSN(mysql.DSN(databaseName, databaseUsername, databasePassword)), 36 | suite.WithDatabasePlaceholderFormat(squirrel.Question), 37 | suite.WithFeatureFilesLocation("../features"), 38 | suite.WithCustomerRepositoryConstructor(newRepository()), 39 | ) 40 | } 41 | 42 | func imageTag() string { 43 | v := os.Getenv("MYSQL_VERSION") 44 | if v == "" { 45 | return defaultVersion 46 | } 47 | 48 | return v 49 | } 50 | 51 | func imageName() string { 52 | img := os.Getenv("MYSQL_DIST") 53 | if img == "" { 54 | return defaultImage 55 | } 56 | 57 | return img 58 | } 59 | -------------------------------------------------------------------------------- /tests/mysql/repository.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "context" 5 | 6 | "go.nhat.io/clock" 7 | 8 | "go.nhat.io/otelsql/tests/suite" 9 | "go.nhat.io/otelsql/tests/suite/customer" 10 | ) 11 | 12 | type repository struct { 13 | db suite.DatabaseExecer 14 | clock clock.Clock 15 | } 16 | 17 | func (r *repository) Find(ctx context.Context, id int) (*customer.Customer, error) { 18 | row, err := r.db.QueryRow(ctx, 19 | "SELECT * FROM customer WHERE id = ? LIMIT 1", 20 | id, 21 | ) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | c := &customer.Customer{} 27 | 28 | if err := row.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 29 | return nil, err 30 | } 31 | 32 | return c, nil 33 | } 34 | 35 | func (r *repository) FindAll(ctx context.Context) ([]customer.Customer, error) { 36 | rows, err := r.db.Query(ctx, "SELECT * FROM customer") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defer func() { 42 | _ = rows.Close() // nolint: errcheck 43 | _ = rows.Err() // nolint: errcheck 44 | }() 45 | 46 | customers := make([]customer.Customer, 0) 47 | 48 | for rows.Next() { 49 | c := customer.Customer{} 50 | 51 | if err := rows.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 52 | return nil, err 53 | } 54 | 55 | customers = append(customers, c) 56 | } 57 | 58 | return customers, nil 59 | } 60 | 61 | func (r *repository) Create(ctx context.Context, user customer.Customer) error { 62 | _, err := r.db.Exec(ctx, 63 | "INSERT INTO customer VALUES (?, ?, ?, ?, ?, ?, ?)", 64 | user.ID, 65 | user.Country, 66 | user.FirstName, 67 | user.LastName, 68 | user.Email, 69 | user.CreatedAt, 70 | user.UpdatedAt, 71 | ) 72 | 73 | return err 74 | } 75 | 76 | func (r *repository) Update(ctx context.Context, user customer.Customer) error { 77 | _, err := r.db.Exec(ctx, 78 | "UPDATE customer SET country = ?, first_name = ?, last_name = ?, email = ?, created_at = ?, updated_at = ? WHERE id = ?", 79 | user.Country, 80 | user.FirstName, 81 | user.LastName, 82 | user.Email, 83 | user.CreatedAt, 84 | r.clock.Now(), 85 | user.ID, 86 | ) 87 | 88 | return err 89 | } 90 | 91 | func (r *repository) Delete(ctx context.Context, id int) error { 92 | _, err := r.db.Exec(ctx, 93 | "DELETE FROM customer WHERE id = ?", 94 | id, 95 | ) 96 | 97 | return err 98 | } 99 | 100 | func newRepository() suite.CustomerRepositoryConstructor { 101 | return func(db suite.DatabaseExecer, c clock.Clock) customer.Repository { 102 | return &repository{ 103 | db: db, 104 | clock: c, 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/mysql/resources/migrations/1_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE customer ( 2 | id SERIAL PRIMARY KEY, 3 | country VARCHAR(2) NOT NULL, 4 | first_name VARCHAR(50) NOT NULL, 5 | last_name VARCHAR(50) NOT NULL, 6 | email VARCHAR(200) NOT NULL, 7 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | updated_at TIMESTAMP 9 | ); 10 | 11 | CREATE INDEX customer_country ON customer(country); 12 | CREATE UNIQUE INDEX customer_email ON customer(email); 13 | -------------------------------------------------------------------------------- /tests/postgres/doc.go: -------------------------------------------------------------------------------- 1 | // Package postgres provides compatibility tests for postgres drivers. 2 | package postgres 3 | -------------------------------------------------------------------------------- /tests/postgres/main_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/Masterminds/squirrel" 8 | _ "github.com/jackc/pgx/v4/stdlib" // Database driver 9 | _ "github.com/jackc/pgx/v5/stdlib" // Database driver 10 | _ "github.com/lib/pq" // Database driver 11 | "go.nhat.io/testcontainers-extra" 12 | pg "go.nhat.io/testcontainers-registry/postgres" 13 | 14 | "go.nhat.io/otelsql/tests/suite" 15 | ) 16 | 17 | const ( 18 | defaultVersion = "12-alpine" 19 | defaultDriver = "pgx/v4" 20 | 21 | databaseName = "otelsql" 22 | databaseUsername = "otelsql" 23 | databasePassword = "OneWrapperToTraceThemAll" 24 | ) 25 | 26 | func TestIntegration(t *testing.T) { 27 | suite.Run(t, 28 | suite.WithTestContainerRequests( 29 | pg.Request(databaseName, databaseUsername, databasePassword, 30 | pg.RunMigrations("file://./resources/migrations/"), 31 | testcontainers.WithImageTag(imageTag()), 32 | ), 33 | ), 34 | suite.WithDatabaseDriver(databaseDriver()), 35 | suite.WithDatabaseDSN(pg.DSN(databaseName, databaseUsername, databasePassword)), 36 | suite.WithDatabasePlaceholderFormat(squirrel.Dollar), 37 | suite.WithFeatureFilesLocation("../features"), 38 | suite.WithCustomerRepositoryConstructor(newRepository()), 39 | ) 40 | } 41 | 42 | func imageTag() string { 43 | v := os.Getenv("POSTGRES_VERSION") 44 | if v == "" { 45 | return defaultVersion 46 | } 47 | 48 | return v 49 | } 50 | 51 | func databaseDriver() string { 52 | v := os.Getenv("POSTGRES_DRIVER") 53 | if v == "" { 54 | return defaultDriver 55 | } 56 | 57 | return v 58 | } 59 | -------------------------------------------------------------------------------- /tests/postgres/repository.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | 6 | "go.nhat.io/clock" 7 | 8 | "go.nhat.io/otelsql/tests/suite" 9 | "go.nhat.io/otelsql/tests/suite/customer" 10 | ) 11 | 12 | type repository struct { 13 | db suite.DatabaseExecer 14 | clock clock.Clock 15 | } 16 | 17 | func (r *repository) Find(ctx context.Context, id int) (*customer.Customer, error) { 18 | row, err := r.db.QueryRow(ctx, 19 | "SELECT * FROM customer WHERE id = $1 LIMIT 1", 20 | id, 21 | ) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | c := &customer.Customer{} 27 | 28 | if err := row.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 29 | return nil, err 30 | } 31 | 32 | return c, nil 33 | } 34 | 35 | func (r *repository) FindAll(ctx context.Context) ([]customer.Customer, error) { 36 | rows, err := r.db.Query(ctx, "SELECT * FROM customer") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defer func() { 42 | _ = rows.Close() // nolint: errcheck 43 | _ = rows.Err() // nolint: errcheck 44 | }() 45 | 46 | customers := make([]customer.Customer, 0) 47 | 48 | for rows.Next() { 49 | c := customer.Customer{} 50 | 51 | if err := rows.Scan(&c.ID, &c.Country, &c.FirstName, &c.LastName, &c.Email, &c.CreatedAt, &c.UpdatedAt); err != nil { 52 | return nil, err 53 | } 54 | 55 | customers = append(customers, c) 56 | } 57 | 58 | return customers, nil 59 | } 60 | 61 | func (r *repository) Create(ctx context.Context, user customer.Customer) error { 62 | _, err := r.db.Exec(ctx, 63 | "INSERT INTO customer VALUES ($1, $2, $3, $4, $5, $6, $7)", 64 | user.ID, 65 | user.Country, 66 | user.FirstName, 67 | user.LastName, 68 | user.Email, 69 | user.CreatedAt, 70 | user.UpdatedAt, 71 | ) 72 | 73 | return err 74 | } 75 | 76 | func (r *repository) Update(ctx context.Context, user customer.Customer) error { 77 | _, err := r.db.Exec(ctx, 78 | "UPDATE customer SET country = $1, first_name = $2, last_name = $3, email = $4, created_at = $5, updated_at = $6 WHERE id = $7", 79 | user.Country, 80 | user.FirstName, 81 | user.LastName, 82 | user.Email, 83 | user.CreatedAt, 84 | r.clock.Now(), 85 | user.ID, 86 | ) 87 | 88 | return err 89 | } 90 | 91 | func (r *repository) Delete(ctx context.Context, id int) error { 92 | _, err := r.db.Exec(ctx, 93 | "DELETE FROM customer WHERE id = $1", 94 | id, 95 | ) 96 | 97 | return err 98 | } 99 | 100 | func newRepository() suite.CustomerRepositoryConstructor { 101 | return func(db suite.DatabaseExecer, c clock.Clock) customer.Repository { 102 | return &repository{ 103 | db: db, 104 | clock: c, 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/postgres/resources/migrations/1_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE customer ( 2 | id SERIAL PRIMARY KEY, 3 | country VARCHAR(2) NOT NULL, 4 | first_name VARCHAR(50) NOT NULL, 5 | last_name VARCHAR(50) NOT NULL, 6 | email VARCHAR(200) NOT NULL, 7 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | updated_at TIMESTAMP 9 | ); 10 | 11 | CREATE INDEX customer_country ON customer(country); 12 | CREATE UNIQUE INDEX customer_email ON customer(email); 13 | -------------------------------------------------------------------------------- /tests/suite/context.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | "github.com/Masterminds/squirrel" 5 | "go.nhat.io/testcontainers-extra" 6 | ) 7 | 8 | type suiteContext struct { 9 | containers []testcontainers.Container 10 | 11 | featureFiles []string 12 | 13 | databaseDriver string 14 | databaseDSN string 15 | databasePlaceholderFormat squirrel.PlaceholderFormat 16 | 17 | customerRepositoryConstructor CustomerRepositoryConstructor 18 | } 19 | -------------------------------------------------------------------------------- /tests/suite/customer/doc.go: -------------------------------------------------------------------------------- 1 | // Package customer provides definition for customer domain. 2 | package customer 3 | -------------------------------------------------------------------------------- /tests/suite/customer/entity.go: -------------------------------------------------------------------------------- 1 | package customer 2 | 3 | import "time" 4 | 5 | // Customer is a customer entity. 6 | type Customer struct { 7 | ID int64 `db:"id" json:"id"` 8 | Country string `db:"country" json:"country"` 9 | FirstName string `db:"first_name" json:"first_name"` 10 | LastName string `db:"last_name" json:"last_name"` 11 | Email string `db:"email" json:"email"` 12 | CreatedAt time.Time `db:"created_at" json:"created_at"` 13 | UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` 14 | } 15 | -------------------------------------------------------------------------------- /tests/suite/customer/repository.go: -------------------------------------------------------------------------------- 1 | package customer 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Repository handles user data. 8 | type Repository interface { 9 | Finder 10 | Creator 11 | Updater 12 | Deleter 13 | } 14 | 15 | // Finder finds customers. 16 | type Finder interface { 17 | Find(ctx context.Context, id int) (*Customer, error) 18 | FindAll(ctx context.Context) ([]Customer, error) 19 | } 20 | 21 | // Creator creates customers. 22 | type Creator interface { 23 | Create(ctx context.Context, customer Customer) error 24 | } 25 | 26 | // Updater updates customers. 27 | type Updater interface { 28 | Update(ctx context.Context, customer Customer) error 29 | } 30 | 31 | // Deleter deletes customers. 32 | type Deleter interface { 33 | Delete(ctx context.Context, id int) error 34 | } 35 | -------------------------------------------------------------------------------- /tests/suite/doc.go: -------------------------------------------------------------------------------- 1 | // Package suite provides a suite for running compatibility tests. 2 | package suite 3 | -------------------------------------------------------------------------------- /tests/suite/observability.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/cucumber/godog" 8 | prom "github.com/prometheus/client_golang/prometheus" 9 | "go.opentelemetry.io/otel" 10 | "go.opentelemetry.io/otel/exporters/prometheus" 11 | metricsdk "go.opentelemetry.io/otel/sdk/metric" 12 | "go.opentelemetry.io/otel/sdk/resource" 13 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 14 | ) 15 | 16 | type observabilityTests struct { 17 | promExporter *prometheus.Exporter 18 | } 19 | 20 | func (t *observabilityTests) RegisterSteps(sc *godog.ScenarioContext) { 21 | // Reset prometheus exporter for every test. 22 | sc.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) { 23 | promExporter, err := newPrometheusExporter() 24 | if err != nil { 25 | return ctx, fmt.Errorf("could not init prometheus exporter: %w", err) 26 | } 27 | 28 | t.promExporter = promExporter 29 | 30 | return ctx, nil 31 | }) 32 | } 33 | 34 | func newObservabilityTests() *observabilityTests { 35 | return &observabilityTests{} 36 | } 37 | 38 | // var defaultHistogramBoundaries = []float64{1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000} 39 | 40 | func newPrometheusExporter() (*prometheus.Exporter, error) { 41 | e, err := prometheus.New( 42 | prometheus.WithRegisterer(prom.NewRegistry()), 43 | ) 44 | if err != nil { 45 | return nil, fmt.Errorf("could not init prometheus exporter: %w", err) 46 | } 47 | 48 | provider := metricsdk.NewMeterProvider( 49 | metricsdk.WithReader(e), 50 | metricsdk.WithResource(resource.NewSchemaless( 51 | semconv.ServiceNameKey.String("otelsqltest"), 52 | )), 53 | ) 54 | 55 | otel.SetMeterProvider(provider) 56 | 57 | return e, nil 58 | } 59 | -------------------------------------------------------------------------------- /tests/suite/options.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | "github.com/Masterminds/squirrel" 5 | "go.nhat.io/testcontainers-extra" 6 | ) 7 | 8 | // Option sets up the test suite. 9 | type Option func(*suite) 10 | 11 | // WithTestContainerRequests appends container requests. 12 | func WithTestContainerRequests(requests ...testcontainers.StartGenericContainerRequest) Option { 13 | return func(s *suite) { 14 | s.containerRequests = requests 15 | } 16 | } 17 | 18 | // WithDatabaseDriver sets the database driver. 19 | func WithDatabaseDriver(driver string) Option { 20 | return func(s *suite) { 21 | s.databaseDriver = driver 22 | } 23 | } 24 | 25 | // WithDatabaseDSN sets the database dsn. 26 | func WithDatabaseDSN(dsn string) Option { 27 | return func(s *suite) { 28 | s.databaseDSN = dsn 29 | } 30 | } 31 | 32 | // WithDatabasePlaceholderFormat sets the database placeholder format. 33 | func WithDatabasePlaceholderFormat(format squirrel.PlaceholderFormat) Option { 34 | return func(s *suite) { 35 | s.databasePlaceholderFormat = format 36 | } 37 | } 38 | 39 | // WithFeatureFilesLocation sets the feature files location. 40 | func WithFeatureFilesLocation(loc string) Option { 41 | return func(s *suite) { 42 | s.featureFilesLocation = loc 43 | } 44 | } 45 | 46 | // WithCustomerRepositoryConstructor sets the constructor. 47 | func WithCustomerRepositoryConstructor(c CustomerRepositoryConstructor) Option { 48 | return func(s *suite) { 49 | s.customerRepositoryConstructor = c 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/suite/random.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "math/rand" 8 | ) 9 | 10 | func randomString(length int) string { 11 | var rngSeed int64 12 | 13 | _ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed) // nolint: errcheck 14 | r := rand.New(rand.NewSource(rngSeed)) // nolint: gosec 15 | 16 | result := make([]byte, length/2) 17 | 18 | _, _ = r.Read(result) 19 | 20 | return hex.EncodeToString(result) 21 | } 22 | -------------------------------------------------------------------------------- /tests/suite/suite.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/Masterminds/squirrel" 13 | "github.com/cucumber/godog" 14 | "github.com/godogx/clocksteps" 15 | "github.com/stretchr/testify/require" 16 | "go.nhat.io/testcontainers-extra" 17 | _ "go.nhat.io/testcontainers-registry" // Let dependabot manage the update. 18 | ) 19 | 20 | type logger func(format string, args ...any) 21 | 22 | // Suite is a test suite. 23 | type Suite interface { 24 | Run(tb testing.TB) 25 | } 26 | 27 | type suite struct { 28 | containerRequests []testcontainers.StartGenericContainerRequest 29 | 30 | featureFilesLocation string 31 | 32 | databaseDriver string 33 | databaseDSN string 34 | databasePlaceholderFormat squirrel.PlaceholderFormat 35 | 36 | customerRepositoryConstructor CustomerRepositoryConstructor 37 | } 38 | 39 | func (s suite) startContainers(runID string, log logger, requests ...testcontainers.StartGenericContainerRequest) ([]testcontainers.Container, error) { 40 | if len(requests) == 0 { 41 | return nil, nil 42 | } 43 | 44 | for i := range requests { 45 | requests[i].Options = append(requests[i].Options, 46 | testcontainers.WithNamePrefix("otelsql"), 47 | testcontainers.WithNameSuffix(runID), 48 | ) 49 | } 50 | 51 | containers, err := testcontainers.StartGenericContainers(context.Background(), requests...) 52 | if err != nil { 53 | log(err.Error()) 54 | } 55 | 56 | return containers, err 57 | } 58 | 59 | func (s suite) stopContainers(tb testing.TB, containers ...testcontainers.Container) { 60 | tb.Helper() 61 | 62 | if err := testcontainers.StopGenericContainers(context.Background(), containers...); err != nil { 63 | tb.Log(err.Error()) 64 | } 65 | } 66 | 67 | func (s suite) getFeatureFiles(location string, log func(format string, args ...any)) ([]string, error) { 68 | entries, err := os.ReadDir(location) 69 | if err != nil { 70 | log("could not read feature files location: %s", err.Error()) 71 | 72 | return nil, err 73 | } 74 | 75 | result := make([]string, 0) 76 | 77 | for _, f := range entries { 78 | if f.IsDir() || !strings.HasSuffix(f.Name(), ".feature") { 79 | continue 80 | } 81 | 82 | result = append(result, filepath.Join(location, f.Name())) 83 | } 84 | 85 | return result, nil 86 | } 87 | 88 | func (s suite) runTests(tb testing.TB, sc suiteContext) error { 89 | tb.Helper() 90 | 91 | db, err := openDBxWithoutInstrumentation(sc.databaseDriver, sc.databaseDSN) 92 | if err != nil { 93 | tb.Logf("could not init database with sqlx: %s", err.Error()) 94 | 95 | return err 96 | } 97 | 98 | out := bytes.NewBuffer(nil) 99 | 100 | clock := clocksteps.New() 101 | otelsqlTests := newObservabilityTests() 102 | customerTests := newCustomerTests(sc.databaseDriver, sc.databaseDSN, sc.customerRepositoryConstructor, clock) 103 | dbm := makeDBManager(db, sc.databasePlaceholderFormat) 104 | 105 | suite := godog.TestSuite{ 106 | Name: "Integration", 107 | ScenarioInitializer: func(sc *godog.ScenarioContext) { 108 | clock.RegisterSteps(sc) 109 | dbm.RegisterSteps(sc) 110 | otelsqlTests.RegisterSteps(sc) 111 | customerTests.RegisterSteps(sc) 112 | }, 113 | Options: &godog.Options{ 114 | Format: "pretty", 115 | Strict: true, 116 | Output: out, 117 | Randomize: time.Now().UTC().UnixNano(), 118 | Paths: sc.featureFiles, 119 | }, 120 | } 121 | 122 | // Run the suite. 123 | if status := suite.Run(); status != 0 { 124 | tb.Fatal(out.String()) 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func (s suite) start(tb testing.TB) (suiteContext, error) { 131 | tb.Helper() 132 | 133 | var ( 134 | sc suiteContext 135 | err error 136 | ) 137 | 138 | // Start containers. 139 | sc.containers, err = s.startContainers(randomString(8), tb.Logf, s.containerRequests...) 140 | if err != nil { 141 | return sc, err 142 | } 143 | 144 | // Setup. 145 | sc.databaseDriver = s.databaseDriver 146 | sc.databaseDSN = os.ExpandEnv(s.databaseDSN) 147 | sc.databasePlaceholderFormat = s.databasePlaceholderFormat 148 | sc.customerRepositoryConstructor = s.customerRepositoryConstructor 149 | 150 | sc.featureFiles, err = s.getFeatureFiles(s.featureFilesLocation, tb.Logf) 151 | if err != nil { 152 | return sc, err 153 | } 154 | 155 | if err := s.runTests(tb, sc); err != nil { 156 | return sc, err 157 | } 158 | 159 | return sc, nil 160 | } 161 | 162 | func (s suite) stop(tb testing.TB, sc suiteContext) suiteContext { 163 | tb.Helper() 164 | 165 | defer s.stopContainers(tb, sc.containers...) 166 | 167 | return sc 168 | } 169 | 170 | func (s suite) Run(tb testing.TB) { 171 | tb.Helper() 172 | 173 | sc, err := s.start(tb) 174 | defer s.stop(tb, sc) 175 | 176 | require.NoError(tb, err) 177 | } 178 | 179 | // New creates a new test suite. 180 | func New(opts ...Option) Suite { 181 | s := suite{ 182 | databasePlaceholderFormat: squirrel.Question, 183 | } 184 | 185 | for _, opt := range opts { 186 | opt(&s) 187 | } 188 | 189 | return s 190 | } 191 | 192 | // Run creates a new test suite and run it. 193 | func Run(tb testing.TB, opts ...Option) { 194 | tb.Helper() 195 | 196 | New(opts...).Run(tb) 197 | } 198 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import "time" 4 | 5 | func millisecondsSince(t time.Time) float64 { 6 | return float64(time.Since(t).Milliseconds()) 7 | } 8 | -------------------------------------------------------------------------------- /tracer_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "go.opentelemetry.io/otel/codes" 11 | ) 12 | 13 | func TestFormatSpanName(t *testing.T) { 14 | t.Parallel() 15 | 16 | actual := formatSpanName(context.Background(), "ping") 17 | expected := "sql:ping" 18 | 19 | assert.Equal(t, expected, actual) 20 | } 21 | 22 | func TestSpanStatusFromError(t *testing.T) { 23 | t.Parallel() 24 | 25 | testCases := []struct { 26 | scenario string 27 | error error 28 | expectedCode codes.Code 29 | expectedDescription string 30 | }{ 31 | { 32 | scenario: "no error", 33 | error: nil, 34 | expectedCode: codes.Ok, 35 | expectedDescription: "", 36 | }, 37 | { 38 | scenario: "no error", 39 | error: errors.New("error"), 40 | expectedCode: codes.Error, 41 | expectedDescription: "error", 42 | }, 43 | } 44 | 45 | for _, tc := range testCases { 46 | tc := tc 47 | t.Run(tc.scenario, func(t *testing.T) { 48 | t.Parallel() 49 | 50 | code, description := spanStatusFromError(tc.error) 51 | 52 | assert.Equal(t, tc.expectedCode, code) 53 | assert.Equal(t, tc.expectedDescription, description) 54 | }) 55 | } 56 | } 57 | 58 | func TestSpanStatusFromErrorIgnoreErrSkip(t *testing.T) { 59 | t.Parallel() 60 | 61 | testCases := []struct { 62 | scenario string 63 | error error 64 | expectedCode codes.Code 65 | expectedDescription string 66 | }{ 67 | { 68 | scenario: "no error", 69 | error: nil, 70 | expectedCode: codes.Ok, 71 | expectedDescription: "", 72 | }, 73 | { 74 | scenario: "skip", 75 | error: driver.ErrSkip, 76 | expectedCode: codes.Ok, 77 | expectedDescription: "", 78 | }, 79 | { 80 | scenario: "no error", 81 | error: errors.New("error"), 82 | expectedCode: codes.Error, 83 | expectedDescription: "error", 84 | }, 85 | } 86 | 87 | for _, tc := range testCases { 88 | tc := tc 89 | t.Run(tc.scenario, func(t *testing.T) { 90 | t.Parallel() 91 | 92 | code, description := spanStatusFromErrorIgnoreErrSkip(tc.error) 93 | 94 | assert.Equal(t, tc.expectedCode, code) 95 | assert.Equal(t, tc.expectedDescription, description) 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | ) 9 | 10 | const ( 11 | metricMethodCommit = "go.sql.commit" 12 | traceMethodCommit = "commit" 13 | metricMethodRollback = "go.sql.rollback" 14 | traceMethodRollback = "rollback" 15 | ) 16 | 17 | var _ driver.Tx = (*tx)(nil) 18 | 19 | type txFuncMiddleware = middleware[txFunc] 20 | 21 | type txFunc func() error 22 | 23 | type tx struct { 24 | commit txFunc 25 | rollback txFunc 26 | } 27 | 28 | func (t tx) Commit() error { 29 | return t.commit() 30 | } 31 | 32 | func (t tx) Rollback() error { 33 | return t.rollback() 34 | } 35 | 36 | func wrapTx(ctx context.Context, parent driver.Tx, r methodRecorder, t methodTracer) driver.Tx { 37 | ctx = trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(ctx)) 38 | 39 | return &tx{ 40 | commit: chainMiddlewares(makeTxFuncMiddlewares(ctx, r, t, metricMethodCommit, traceMethodCommit), parent.Commit), 41 | rollback: chainMiddlewares(makeTxFuncMiddlewares(ctx, r, t, metricMethodRollback, traceMethodRollback), parent.Rollback), 42 | } 43 | } 44 | 45 | func nopTxFunc() error { 46 | return nil 47 | } 48 | 49 | func txStats(ctx context.Context, r methodRecorder, method string) txFuncMiddleware { 50 | return func(next txFunc) txFunc { 51 | return func() (err error) { 52 | end := r.Record(ctx, method) 53 | 54 | defer func() { 55 | end(err) 56 | }() 57 | 58 | return next() 59 | } 60 | } 61 | } 62 | 63 | func txTrace(ctx context.Context, t methodTracer, method string) txFuncMiddleware { 64 | return func(next txFunc) txFunc { 65 | return func() (err error) { 66 | _, end := t.MustTrace(ctx, method) 67 | 68 | defer func() { 69 | end(err) 70 | }() 71 | 72 | return next() 73 | } 74 | } 75 | } 76 | 77 | func makeTxFuncMiddlewares(ctx context.Context, r methodRecorder, t methodTracer, metricMethod string, traceMethod string) []txFuncMiddleware { 78 | middlewares := make([]txFuncMiddleware, 0, 2) 79 | middlewares = append(middlewares, txStats(ctx, r, metricMethod)) 80 | 81 | if t != nil { 82 | middlewares = append(middlewares, txTrace(ctx, t, traceMethod)) 83 | } 84 | 85 | return middlewares 86 | } 87 | -------------------------------------------------------------------------------- /transaction_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 11 | 12 | "go.nhat.io/otelsql/internal/test/oteltest" 13 | ) 14 | 15 | func TestChainTxFuncMiddlewares_NoMiddleware(t *testing.T) { 16 | t.Parallel() 17 | 18 | f := chainMiddlewares(nil, nopTxFunc) 19 | 20 | err := f() 21 | 22 | assert.NoError(t, err) 23 | } 24 | 25 | func TestChainTxFuncMiddlewares(t *testing.T) { 26 | t.Parallel() 27 | 28 | stack := make([]string, 0) 29 | 30 | pushTxFunc := func(s string) txFunc { 31 | return func() error { 32 | stack = append(stack, s) 33 | 34 | return nil 35 | } 36 | } 37 | 38 | pushTxFuncMiddleware := func(s string) txFuncMiddleware { 39 | return func(next txFunc) txFunc { 40 | return func() error { 41 | stack = append(stack, s) 42 | 43 | return next() 44 | } 45 | } 46 | } 47 | 48 | f := chainMiddlewares( 49 | []txFuncMiddleware{ 50 | pushTxFuncMiddleware("outer"), 51 | pushTxFuncMiddleware("inner"), 52 | }, 53 | pushTxFunc("end"), 54 | ) 55 | err := f() 56 | 57 | assert.NoError(t, err) 58 | 59 | expected := []string{"outer", "inner", "end"} 60 | 61 | assert.Equal(t, expected, stack) 62 | } 63 | 64 | func TestTxStats(t *testing.T) { 65 | t.Parallel() 66 | 67 | testCases := []struct { 68 | scenario string 69 | beginner txFunc 70 | expected string 71 | }{ 72 | { 73 | scenario: "error", 74 | beginner: func() error { 75 | return errors.New("error") 76 | }, 77 | expected: `[ 78 | { 79 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=tx_test,db.instance=test,db.operation=go.sql.commit,db.sql.error=error,db.sql.status=ERROR,db.system=other_sql}", 80 | "Sum": 1 81 | }, 82 | { 83 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=tx_test,db.instance=test,db.operation=go.sql.commit,db.sql.error=error,db.sql.status=ERROR,db.system=other_sql}", 84 | "Sum": "", 85 | "Count": 1 86 | } 87 | ]`, 88 | }, 89 | { 90 | scenario: "no error", 91 | beginner: nopTxFunc, 92 | expected: `[ 93 | { 94 | "Name": "db.sql.client.calls{service.name=otelsql,instrumentation.name=tx_test,db.instance=test,db.operation=go.sql.commit,db.sql.status=OK,db.system=other_sql}", 95 | "Sum": 1 96 | }, 97 | { 98 | "Name": "db.sql.client.latency{service.name=otelsql,instrumentation.name=tx_test,db.instance=test,db.operation=go.sql.commit,db.sql.status=OK,db.system=other_sql}", 99 | "Sum": "", 100 | "Count": 1, 101 | "Count": 1 102 | } 103 | ]`, 104 | }, 105 | } 106 | 107 | for _, tc := range testCases { 108 | tc := tc 109 | t.Run(tc.scenario, func(t *testing.T) { 110 | t.Parallel() 111 | 112 | oteltest.New(oteltest.MetricsEqualJSON(tc.expected)). 113 | Run(t, func(s oteltest.SuiteContext) { 114 | meter := s.MeterProvider().Meter("tx_test") 115 | 116 | histogram, err := meter.Float64Histogram(dbSQLClientLatencyMs) 117 | require.NoError(t, err) 118 | 119 | count, err := meter.Int64Counter(dbSQLClientCalls) 120 | require.NoError(t, err) 121 | 122 | r := newMethodRecorder(histogram.Record, count.Add, 123 | semconv.DBSystemOtherSQL, 124 | dbInstance.String("test"), 125 | ) 126 | 127 | f := chainMiddlewares([]txFuncMiddleware{ 128 | txStats(context.Background(), r, metricMethodCommit), 129 | }, tc.beginner) 130 | 131 | _ = f() // nolint: errcheck 132 | }) 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import "database/sql/driver" 4 | 5 | func valuesToNamedValues(values []driver.Value) []driver.NamedValue { 6 | if values == nil { 7 | return nil 8 | } 9 | 10 | namedValues := make([]driver.NamedValue, len(values)) 11 | 12 | for i, v := range values { 13 | namedValues[i] = driver.NamedValue{ 14 | Ordinal: i + 1, 15 | Value: v, 16 | } 17 | } 18 | 19 | return namedValues 20 | } 21 | 22 | func namedValuesToValues(namedValues []driver.NamedValue) []driver.Value { 23 | if namedValues == nil { 24 | return nil 25 | } 26 | 27 | values := make([]driver.Value, len(namedValues)) 28 | 29 | for i, v := range namedValues { 30 | values[i] = v.Value 31 | } 32 | 33 | return values 34 | } 35 | -------------------------------------------------------------------------------- /value_internal_test.go: -------------------------------------------------------------------------------- 1 | package otelsql 2 | 3 | import ( 4 | "database/sql/driver" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestValuesToNamedValues(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := []struct { 14 | scenario string 15 | values []driver.Value 16 | expected []driver.NamedValue 17 | }{ 18 | { 19 | scenario: "nil", 20 | values: nil, 21 | expected: nil, 22 | }, 23 | { 24 | scenario: "empty", 25 | values: []driver.Value{}, 26 | expected: []driver.NamedValue{}, 27 | }, 28 | { 29 | scenario: "not empty", 30 | values: []driver.Value{"foobar", 42}, 31 | expected: []driver.NamedValue{ 32 | {Ordinal: 1, Value: "foobar"}, 33 | {Ordinal: 2, Value: 42}, 34 | }, 35 | }, 36 | } 37 | 38 | for _, tc := range testCases { 39 | tc := tc 40 | t.Run(tc.scenario, func(t *testing.T) { 41 | t.Parallel() 42 | 43 | actual := valuesToNamedValues(tc.values) 44 | 45 | assert.Equal(t, tc.expected, actual) 46 | }) 47 | } 48 | } 49 | 50 | func TestNamedValuesToValues(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := []struct { 54 | scenario string 55 | values []driver.NamedValue 56 | expected []driver.Value 57 | }{ 58 | { 59 | scenario: "nil", 60 | values: nil, 61 | expected: nil, 62 | }, 63 | { 64 | scenario: "empty", 65 | values: []driver.NamedValue{}, 66 | expected: []driver.Value{}, 67 | }, 68 | { 69 | scenario: "not empty", 70 | values: []driver.NamedValue{ 71 | {Ordinal: 1, Value: "foobar"}, 72 | {Ordinal: 2, Value: 42}, 73 | }, 74 | expected: []driver.Value{"foobar", 42}, 75 | }, 76 | } 77 | 78 | for _, tc := range testCases { 79 | tc := tc 80 | t.Run(tc.scenario, func(t *testing.T) { 81 | t.Parallel() 82 | 83 | actual := namedValuesToValues(tc.values) 84 | 85 | assert.Equal(t, tc.expected, actual) 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Code generated by release workflow. DO NOT EDIT. 2 | 3 | package otelsql 4 | 5 | // Version is the current release version of the otelsql instrumentation. 6 | func Version() string { 7 | return "0.14.0" 8 | } 9 | 10 | // SemVersion is the semantic version to be supplied to tracer/meter creation. 11 | func SemVersion() string { 12 | return "semver:" + Version() 13 | } 14 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package otelsql_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "go.nhat.io/otelsql" 10 | ) 11 | 12 | func TestSemVersion(t *testing.T) { 13 | t.Parallel() 14 | 15 | actual := otelsql.SemVersion() 16 | expected := fmt.Sprintf("semver:%s", otelsql.Version()) 17 | 18 | assert.Equal(t, expected, actual) 19 | } 20 | --------------------------------------------------------------------------------