Repository: davidB/tracing-opentelemetry-instrumentation-sdk Branch: main Commit: db954acd415a Files: 114 Total size: 344.3 KB Directory structure: gitextract_m0cxzsml/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── ci.yml │ ├── mega-linter.yml │ └── release-plz.yml ├── .gitignore ├── .mega-linter.yml ├── .mise.toml ├── .vscode/ │ └── settings.json ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── axum-tracing-opentelemetry/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── lib.rs │ └── middleware/ │ ├── mod.rs │ ├── response_injector.rs │ └── trace_extractor.rs ├── cliff.toml ├── deny.toml ├── examples/ │ ├── axum-otlp/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── bug_234_tls/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── grpc/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── helloworld.proto │ │ └── src/ │ │ ├── client.rs │ │ ├── generated/ │ │ │ └── helloworld.rs │ │ └── server.rs │ ├── init-tracing-with/ │ │ ├── .cargo/ │ │ │ └── config.toml │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── load/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ └── logging/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── fake-opentelemetry-collector/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── common.rs │ │ ├── lib.rs │ │ ├── logs.rs │ │ ├── metrics.rs │ │ └── trace.rs │ └── tests/ │ ├── demo_log.rs │ ├── demo_metrics.rs │ ├── demo_trace.rs │ └── snapshots/ │ ├── demo_log__demo_fake_logger_and_collector.snap │ ├── demo_metrics__demo_fake_meter_and_collector.snap │ └── demo_trace__demo_fake_tracer_and_collector.snap ├── init-tracing-opentelemetry/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── config.rs │ ├── error.rs │ ├── formats.rs │ ├── lib.rs │ ├── otlp/ │ │ ├── logs.rs │ │ ├── metrics.rs │ │ ├── mod.rs │ │ └── traces.rs │ ├── resource.rs │ ├── stdio.rs │ └── tracing_subscriber_ext.rs ├── release-plz.toml ├── renovate.json5 ├── rust-toolchain.toml ├── testing-tracing-opentelemetry/ │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ └── snapshots/ │ ├── testing_tracing_opentelemetry__call_with_w3c_trace.snap │ ├── testing_tracing_opentelemetry__call_with_w3c_trace_otel_spans.snap │ ├── testing_tracing_opentelemetry__empty_http_route_for_nonexisting_route.snap │ ├── testing_tracing_opentelemetry__empty_http_route_for_nonexisting_route_otel_spans.snap │ ├── testing_tracing_opentelemetry__extract_route_from_nested.snap │ ├── testing_tracing_opentelemetry__extract_route_from_nested_otel_spans.snap │ ├── testing_tracing_opentelemetry__filled_http_headers.snap │ ├── testing_tracing_opentelemetry__filled_http_headers_otel_spans.snap │ ├── testing_tracing_opentelemetry__filled_http_route_for_existing_route.snap │ ├── testing_tracing_opentelemetry__filled_http_route_for_existing_route_otel_spans.snap │ ├── testing_tracing_opentelemetry__status_code_on_close_for_error.snap │ ├── testing_tracing_opentelemetry__status_code_on_close_for_error_otel_spans.snap │ ├── testing_tracing_opentelemetry__status_code_on_close_for_ok.snap │ ├── testing_tracing_opentelemetry__status_code_on_close_for_ok_otel_spans.snap │ ├── testing_tracing_opentelemetry__trace_id_in_child_span.snap │ ├── testing_tracing_opentelemetry__trace_id_in_child_span_for_remote.snap │ ├── testing_tracing_opentelemetry__trace_id_in_child_span_for_remote_otel_spans.snap │ └── testing_tracing_opentelemetry__trace_id_in_child_span_otel_spans.snap ├── tonic-tracing-opentelemetry/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── lib.rs │ └── middleware/ │ ├── client.rs │ ├── filters.rs │ ├── mod.rs │ └── server.rs └── tracing-opentelemetry-instrumentation-sdk/ ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src/ ├── http/ │ ├── grpc.rs │ ├── grpc_client.rs │ ├── grpc_server.rs │ ├── http_server.rs │ ├── mod.rs │ ├── opentelemetry_http.rs │ └── tools.rs ├── lib.rs └── span_type.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ root = true [*] indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 [*.conf] indent_size = 2 [*.md] #inside code block, indentation could be anything indent_size = unset [*.py] indent_size = 4 # 88 is the default for black formatter # 79 is PEP8's recommendation # 119 is django's recommendation max_line_length = 88 [*.rs] # https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/guide.md indent_size = 4 # officially the limit is 100, but we have long url (unsplittable) in comment max_line_length = 200 [{*.bazel,*.bzl,BUILD,WORKSPACE}] indent_size = 4 [*.java] # try to align with https://github.com/diffplug/spotless (https://github.com/google/google-java-format) indent_size = 4 max_line_length = 100 # The JSON files contain newlines inconsistently [*.json] insert_final_newline = unset [**/vendor/**] indent_style = unset indent_size = unset insert_final_newline = unset # Minified JavaScript files shouldn't be changed [**.min.js] indent_style = unset indent_size = unset insert_final_newline = unset # Makefiles always use tabs for indentation [Makefile] indent_style = tab indent_size = 4 [justfile] indent_style = space indent_size = 4 # Batch files use tabs for indentation [*.bat] indent_style = tab indent_size = 4 ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: [davidB] # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry # liberapay: # Replace with a single Liberapay username # issuehunt: # Replace with a single IssueHunt username # otechie: # Replace with a single Otechie username # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/workflows/ci.yml ================================================ --- name: ci on: pull_request: branches-ignore: - "release-plz-*" push: branches: - main - master workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest # - macos-latest env: CARGO_TERM_COLOR: always RUST_BACKTRACE: full SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v6 - uses: mozilla-actions/sccache-action@v0.0.9 - uses: jdx/mise-action@v4 with: # version 2025.5.11, a symlink is created for rust setup # without cache, missing components are installed # with cache, nothing is installed, but as rust tool is symlinked, it is not cached => missing components failure cache: false cache_save: false experimental: true - run: mise run --jobs 1 ci #- run: mise run test-each-feature ================================================ FILE: .github/workflows/mega-linter.yml ================================================ # MegaLinter GitHub Action configuration file # More info at https://megalinter.io --- name: MegaLinter # Trigger mega-linter at every push. Action will also be visible from Pull # Requests to main on: push: branches: [main] pull_request: workflow_dispatch: # Comment env block if you do not want to apply fixes env: # Apply linter fixes configuration # # When active, APPLY_FIXES must also be defined as environment variable # (in github/workflows/mega-linter.yml or other CI tool) APPLY_FIXES: all # Decide which event triggers application of fixes in a commit or a PR # (pull_request, push, all) APPLY_FIXES_EVENT: pull_request # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) # or posted in a PR (pull_request) APPLY_FIXES_MODE: commit concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true # Give the default GITHUB_TOKEN write permission to commit and push, comment # issues & post new PR; remove the ones you do not need permissions: contents: write issues: write pull-requests: write jobs: megalinter: name: MegaLinter runs-on: ubuntu-latest steps: # Git Checkout - name: Checkout Code uses: actions/checkout@v6 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to # improve performance fetch-depth: 0 # MegaLinter - name: MegaLinter # You can override MegaLinter flavor used to have faster performances # More info at https://megalinter.io/flavors/ uses: oxsecurity/megalinter/flavors/documentation@v9 id: ml # All available variables are described in documentation # https://megalinter.io/configuration/ env: # Validates all source when push on main, else just the git diff with # main. Override with true if you always want to lint all sources # # To validate the entire codebase, set to: # VALIDATE_ALL_CODEBASE: true # # To validate only diff with main, set to: # VALIDATE_ALL_CODEBASE: >- # ${{ # github.event_name == 'push' && # contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) # }} VALIDATE_ALL_CODEBASE: >- ${{ github.event_name == 'push' && contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE # .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY # Uncomment to disable copy-paste and spell checks # DISABLE: COPYPASTE,SPELL # Upload MegaLinter artifacts - name: Archive production artifacts uses: actions/upload-artifact@v7 if: success() || failure() with: name: MegaLinter reports path: | megalinter-reports mega-linter.log # Set APPLY_FIXES_IF var for use in future steps - name: Set APPLY_FIXES_IF var run: | printf 'APPLY_FIXES_IF=%s\n' "${{ steps.ml.outputs.has_updated_sources == 1 && ( env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name ) && ( github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) }}" >> "${GITHUB_ENV}" # Set APPLY_FIXES_IF_* vars for use in future steps - name: Set APPLY_FIXES_IF_* vars run: | printf 'APPLY_FIXES_IF_PR=%s\n' "${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'pull_request' }}" >> "${GITHUB_ENV}" printf 'APPLY_FIXES_IF_COMMIT=%s\n' "${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'commit' && (!contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)) }}" >> "${GITHUB_ENV}" # Create pull request if applicable # (for now works only on PR from same repository, not from forks) - name: Create Pull Request with applied fixes uses: peter-evans/create-pull-request@v8 id: cpr if: env.APPLY_FIXES_IF_PR == 'true' with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} commit-message: "[MegaLinter] Apply linters automatic fixes" title: "[MegaLinter] Apply linters automatic fixes" labels: bot - name: Create PR output if: env.APPLY_FIXES_IF_PR == 'true' run: | echo "PR Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "PR URL - ${{ steps.cpr.outputs.pull-request-url }}" # Push new commit if applicable # (for now works only on PR from same repository, not from forks) - name: Prepare commit if: env.APPLY_FIXES_IF_COMMIT == 'true' run: sudo chown -Rc $UID .git/ - name: Commit and push applied linter fixes uses: stefanzweifel/git-auto-commit-action@v7 if: env.APPLY_FIXES_IF_COMMIT == 'true' with: branch: >- ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} commit_message: "[MegaLinter] Apply linters fixes" commit_user_name: megalinter-bot commit_user_email: nicolas.vuillamy@ox.security ================================================ FILE: .github/workflows/release-plz.yml ================================================ name: release-plz permissions: pull-requests: write contents: write on: workflow_dispatch: push: branches: - main concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true jobs: release-plz: name: Release-plz runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} ================================================ FILE: .gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/git,bazel,vim,emacs,visualstudiocode,jetbrains+all,helm,rust # Edit at https://www.toptal.com/developers/gitignore?templates=git,bazel,vim,emacs,visualstudiocode,jetbrains+all,helm,rust ### Bazel ### # gitignore template for Bazel build system # website: https://bazel.build/ # Ignore all bazel-* symlinks. There is no full list since this can change # based on the name of the directory bazel is cloned into. /bazel-* # Directories for the Bazel IntelliJ plugin containing the generated # IntelliJ project files and plugin configuration. Seperate directories are # for the IntelliJ, Android Studio and CLion versions of the plugin. /.ijwb/ /.aswb/ /.clwb/ ### Emacs ### # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### Git ### # Created by git for backups. To disable backups in Git: # $ git config --global mergetool.keepBackup false *.orig # Created by git when using merge tools for conflicts *.BACKUP.* *.BASE.* *.LOCAL.* *.REMOTE.* *_BACKUP_*.txt *_BASE_*.txt *_LOCAL_*.txt *_REMOTE_*.txt ### Helm ### # Chart dependencies **/charts/*.tgz ### JetBrains+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### JetBrains+all Patch ### # Ignore everything but code style settings and run configurations # that are supposed to be shared within teams. .idea/* !.idea/codeStyles !.idea/runConfigurations ### Rust ### # Generated by Cargo # will have compiled files and executables debug/ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb ### Vim ### # Swap [._]*.s[a-v][a-z] !*.svg # comment out if you don't need vector files [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim Sessionx.vim # Temporary .netrwhist # Auto-generated tag files tags # Persistent undo [._]*.un~ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix ### VisualStudioCode Patch ### # Ignore all local history of files .history .ionide # Support for Project snippet scope # End of https://www.toptal.com/developers/gitignore/api/git,bazel,vim,emacs,visualstudiocode,jetbrains+all,helm,rust .envrc # ignore downloaded charts *.tgz *.off # ignore report from MegaLinter report/ megalinter-reports/ tempo-data ================================================ FILE: .mega-linter.yml ================================================ # Configuration file for MegaLinter # See all available variables at https://megalinter.github.io/configuration/ and in linters documentation APPLY_FIXES: all # all, none, or list of linter keys # ENABLE: # If you use ENABLE variable, all other languages/formats/tooling-formats will be disabled by default # ENABLE_LINTERS: # If you use ENABLE_LINTERS variable, all other linters will be disabled by default DISABLE: - COPYPASTE # Comment to enable checks of excessive copy-pastes - SPELL # Comment to enable checks of spelling mistakes DISABLE_LINTERS: - MARKDOWN_MARKDOWN_LINK_CHECK - DOCKERFILE_DOCKERFILELINT - RUST_CLIPPY - REPOSITORY_DEVSKIM - REPOSITORY_KICS - REPOSITORY_TRIVY SHOW_ELAPSED_TIME: true FILEIO_REPORTER: false # DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass FILTER_REGEX_EXCLUDE: "(\\.lock)|(\\.ndjson)|(\\.pdf)|(\\.csv)|(\\.zip)|(\\.tar)|(\\.ipynb)|(license.*)|(LICENSE.*)|(.*CHANGELOG.*)" SPELL_FILTER_REGEX_INCLUDE: '\\.md$' RUST_CLIPPY_ARGUMENTS: --workspace --all-features --all-targets -- --deny warnings --allow deprecated --allow unknown-lints ================================================ FILE: .mise.toml ================================================ [env] OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "http://127.0.0.1:4317" OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "grpc" OTEL_TRACES_SAMPLER = "always_on" # RUST_LOG = "warn,otel::setup=debug" # [settings] # auto_install_disable_tools = [ # "cargo:cargo-deny", # "cargo:cargo-hack", # "cargo:cargo-insta", # "cargo:cargo-nextest", # "cargo:cargo-release", # "git-cliff", # ] [tools] rust = { version = "1.91.0", profile = "minimal", components = "rustfmt,clippy" } # the rust tool stack (with cargo, fmt, clippy) to build source grpcurl = "1.9" protoc = "34.1" # grpc-health-probe = "*" # sccache = "0.5" # cargo-binstall allow to download (insteal of build) "cargo:*" # - do not use cargo-binstall" (it's a special name used by mise) # - "aqua:cargo-bins/cargo-binstall" allow to download the binary "aqua:cargo-bins/cargo-binstall" = "1" "cargo:cargo-deny" = "latest" "cargo:cargo-nextest" = "latest" "cargo:cargo-insta" = "latest" "cargo:cargo-release" = "latest" "cargo:cargo-hack" = "latest" "git-cliff" = "latest" [tasks.autofix] description = "Automatically fix some linting issues & format code" depends_post = ["format"] run = "cargo clippy --all-features --fix --allow-dirty" [tasks.format] alias = "fmt" description = "Format the code and sort dependencies" run = [ "cargo fmt", # "cargo sort --workspace --grouped" ] [tasks.deny] description = "Run cargo deny checks" run = "cargo deny check" [tasks.check] description = "Check code with all feature combinations" run = "cargo hack check --each-feature --no-dev-deps" wait_for = ["test", "lint"] [tasks.lint] description = "Lint the rust code" run = [ "cargo fmt --all -- --check", "cargo clippy --workspace --all-features --all-targets -- --deny warnings --allow deprecated --allow unknown-lints", ] wait_for = ["deny"] [tasks.megalinter] description = "Run megalinter in container" run = 'docker run --pull always --rm -it -v "$PWD:/tmp/lint:rw" "oxsecurity/megalinter-documentation:v8"' [tasks.test] description = "Launch tests" run = [ "cargo nextest run", "cargo test --doc", ] wait_for = ["lint"] [tasks."test:review"] description = "Launch snapshot test and review result (accept or reject)" run = [ "cargo insta review", ] wait_for = ["lint"] [tasks.test-each-feature] description = "Test each feature separately" run = "cargo hack test --each-feature -- --test-threads=1" [tasks.set-version] description = "Set version across all workspace crates (Usage: mise run set-version )" run = ''' #!/usr/bin/env bash version="{{arg(name="version")}}" sed -i "s/^version = .*/version = \"$version\"/" Cargo.toml release-plz set-version axum-tracing-opentelemetry@"$version" release-plz set-version fake-opentelemetry-collector@"$version" release-plz set-version init-tracing-opentelemetry@"$version" # release-plz set-version testing-tracing-opentelemetry@"$version" release-plz set-version tonic-tracing-opentelemetry@"$version" release-plz set-version tracing-opentelemetry-instrumentation-sdk@"$version" ''' [tasks.run-otel-desktop-viewer] description = "Run otel-desktop-viewer as receiver and viewer of otel trace" run = [ "# Viewer: open http://localhost:8000", "docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 ghcr.io/ctrlspice/otel-desktop-viewer:latest-amd64", ] [tasks.run-jaeger] description = "Run Jaeger all-in-one container" run = [ "# Viewer: open http://localhost:16686", ''' docker run --rm --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 5778:5778 \ -p 9411:9411 \ cr.jaegertracing.io/jaegertracing/jaeger:latest ''' ] [tasks.run-example-grpc-server] description = "Run gRPC server example" run = "cd examples/grpc && OTEL_SERVICE_NAME=grpc-server cargo run --bin server" [tasks.run-example-grpc-client] description = "Run gRPC client example" run = ''' grpcurl -plaintext 127.0.0.1:50051 list cd examples/grpc && OTEL_SERVICE_NAME=grpc-client cargo run --bin client ''' [tasks.run-example-axum-otlp-server] description = "Run axum-otlp server example" run = "cd examples/axum-otlp && OTEL_SERVICE_NAME=axum-otlp-4317 cargo run" [tasks.run-example-axum-otlp-server-http] description = "Run axum-otlp server example over HTTP" run = 'cd examples/axum-otlp && OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4318/v1/traces" OTEL_SERVICE_NAME=axum-otlp-4318 cargo run --features otlp-over-http' [tasks.run-example-http-server] description = "Run HTTP server example (alias for axum-otlp)" depends = ["run-example-axum-otlp-server"] [tasks.run-example-http-client] description = "Run HTTP client example" run = ''' # curl -i http://127.0.0.1:3003/health curl -i http://127.0.0.1:3003/ ''' [tasks.run-example-load] description = "Run load test example" run = "cd examples/load && cargo run --release 2>/dev/null" [tasks.ci] depends = ["check", "lint", "test", "deny"] ================================================ FILE: .vscode/settings.json ================================================ { "cSpell.words": [ "insta", "opentelemetry", "OTEL", "OTLP", "sdktrace", "semcov" ], "rust-analyzer.linkedProjects": [ "./Cargo.toml", "./axum-tracing-opentelemetry/Cargo.toml", "./axum-tracing-opentelemetry/Cargo.toml" ] } ================================================ FILE: .yamllint.yml ================================================ ########################################### # These are the rules used for # # linting all the yaml files in the stack # # NOTE: # # You can disable line with: # # # yamllint disable-line # ########################################### extends: default rules: document-start: disable new-lines: level: warning type: unix line-length: max: 800 comments: min-spaces-from-content: 1 # Used to follow prettier standard: https://github.com/prettier/prettier/pull/10926 indentation: indent-sequences: consistent ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added ### Changed - ⬆️ upgrade to opentelemetry 0.24 (and related dependencies) ### Fixed ## [0.17.0] - 2024-02-11 ### Added - ✨ add support for logfmt into `tracing_subscriber_ext::init_subscribers()` ([580d709](580d709161554b5888697604a6ec125a69503b09)) ### Changed - 📝 update CHANGELOG ([d3609ac](d3609ac2cc699d3a24fbf89754053cc8e938e3bf)) - 📝 update link to homepage ([3e081fb](3e081fbd3a410fb9abfbdc0cc7a5a2953fee823a)) - 💄 fix display of OTEL_TRACES_SAMPLER ([62d9c2a](62d9c2a7af020015949e1eaf308d365124aad43b)) ### Fixed - 🐛 on grpc when no status code into header, fallback to OK (previously Unkown) ([f1a23c4](f1a23c4cdeba8abcea598a2d4305dbe6e1a10edf)) ## [0.16.0] - 2023-12-30 ### Added - ✨ add support for OTLP headers from environment (#110) ([ccd123b](ccd123b6d7de9c1f10d3d861cb8494db9ed201ee)) ### Changed - 📝 update CHANGELOG ([319b1eb](319b1eb17cc8876d3b7f999a4e1d5b4f534d2816)) - 📝 Update link to changelog, remove homepage, ... ([7f38094](7f380949f73c76a315779db727b75be32211804d)) - ➖ remove dependency to opentelemetry-http ([e049fb0](e049fb0e0c67140b3252bf465aa3c74e6838400d)) - ⬆️ upgrade dependencies for axum-0.7 ([d4ad2d3](d4ad2d31bf8787b8c99332f6b8a7e44e34088886)) - 📝 update example in doc ([b74c686](b74c68604ab359b19d4e43da1a3b0514e1ec2e68)) ### Fixed - 🐛 fix compilation & linter ([24d1eca](24d1eca18a2f85bd2fda98389583684a89d42e7c)) ## [0.15.0] - 2023-11-25 ### Added - ✨ add attribute `rpc.grpc.status_code` ([d885954](d8859542f80cf0df365ee18c3fcce654e2e1a843)) ### Changed - ⬆️ upgrade to openteletry 0.21 (and related dependencies) ([21ceb34](21ceb3450973b288743c9fc026cc45072364bb5e)) ### Fixed - 🐛 attribute `http.response.status_code` should of type `int` ([6ff9209](6ff9209175101ed767fd5eee0f5a33f663755dce)) ## [0.14.0] - 2023-09-04 ### Added - ✨ enable simple basic grpc tls endpoint (#85) ([ecf4f9d](ecf4f9decce5e14766e6e7c24138bcc3519cd540)) ### Changed - ✏️ fix typo in homepage of init-tracing-opentelemetry ([9cfbaff](9cfbaff8f344e3ba918c7b0fa2587d0d18945172)) - ⬆️ bump tracing-opentelemetry from 0.20 to 0.21 ([6763c41](6763c41ba06e34fe1382e1f797c539e57cbe9cf5)) - ⬆️ bump tonic from 0.9 to 0.10 in tonic-tracing-opentelemetrty ([f33bfe6](f33bfe6f77fd1013497d79f147ce2732d3f4e3ea)) ## [0.13.0] - 2023-08-06 ### Added - Feat: add span.type=web on spans ([d76017f](d76017f797b5b9cf2a649824aaea07c81cf84dcf)) - Feat: add span_type enum and documentation ([4871359](487135955342241b2633968c2162415159b9cdab)) ### Changed - ⬆️ upgrade to opentelemetry 0.20 (and related dependencies) ([8b8281e](8b8281ee5e938143379db7d5ef645a830ba87c51)) ## [0.12.0] - 2023-07-02 ### Changed - 📝 update README ([400adeb](400adeb7b1105f0c197a29b6a27ec35fe4b1f722)) ## [0.12.0-alpha.2] - 2023-06-28 ### Added - 💥 use `otel::tracing` as target for trace instead on the name of the crate ([1fda7c3](1fda7c3d566d4a622710116616d9d28680b7b475)) - ✨ introduce new crate `tracing-opentelemetry-instrumentation-sdk` ([51c45ae](51c45ae5f892e0efbea0ce957d3f3a7524bfe927)) - ✨ grpc server layer can use a filter function to not create trace for some endpoint ([2f3ca50](2f3ca5045ab43fcd5c2f3985f9117c9940d5f3ae)) - 💥 rewrite axum-tracing-opentelemetry ([661b891](661b8917d61b52e8d682863e75bece9ad76e9f9b)) ### Changed - ⚡️ tag as `inline` some helpers function ([753b1a7](753b1a72ece620a46461f7860360ebc347f518bb)) ### Fixed - 🐛 grpc client set the span context during async children processing ([cec0ce5](cec0ce531fbca3caf371ee290593e9cf5e226bf7)) - 🐛 grpc server set the span context during async children processing ([83d88e4](83d88e466049c4613cdd10e2d6668cd3a3d0428e)) ## [0.12.0-alpha.1] - 2023-06-14 ### Added - ✨ add basic filtering for axum-tracing-opentelemetry ([bb510a3](bb510a32148182090264d5be9d1c9abe21895083)) ### Changed - 📝 add notes about how to release the workspace ([d1abae1](d1abae15855cfb6fb3d058fbb134f31da82018e3)) ## [0.12.0-alpha.0] - 2023-06-14 ### Added - ✨ extract `fake-opentelemetry-collector` ([25becbb](25becbb6633336c189cb2ab02ff94f7530e8ac57)) - ✨ start the tonic-tracing-opentelemetry ([43c179f](43c179f28aa295a81428d2b08ebae83397329943)) - ✨ start the testing-tracing-opentelemetry ([d7ecb0d](d7ecb0dd416fd4d7d78e51abaa8d10a4c0fbb63a)) ### Changed - ➖ remove more unused dependencies ([46793cf](46793cf708952e21ee316dcedaf1873acf175600)) ## [0.11.0] - 2023-06-11 ### Added - ✨ add a mock_collector server to to collect trace ([b36f5b1](b36f5b1557963d5678f8a337e0bf45606fb03dcf)) ### Changed - ⬆️ upgrade opentelemetry to 0.19 (and related dependencies) ([36b52a0](36b52a0bad4babfc8ace5fcaab79e897907890d3)) - ⬆️ upgrade opentelemetry to 0.19 (and related dependencies) (2) ([b7a2a0e](b7a2a0ed9990c35424335e6cf71fa7e28ba1e60b)) - ⬆️ upgrade opentelemetry to 0.19 (and related dependencies) (3) ([b8719a2](b8719a2912edac8e6556774530fbf99afb82a955)) ### Fixed - 🐛 fix features dependencies ([bdc949d](bdc949d2d0f1eafe0e44ecdbf4607f040150641d)) - Fix: fallback to req uri path for nested route (we can not get matched router in nested router handler) ([36a4302](36a43025ba54721dbb41306086a9135b80350f6c)) - 🐛 generate root opentelemetry span with valid spanId ([c5738a6](c5738a6a4586f9cabd66330335c8353b528498ed)) ## [0.10.0] - 2023-02-26 ### Added - 💥 default configuration for otlp Sampler is now longer hardcoded to `always_on`, but read environment variables `OTEL_TRACES_SAMPLER`, `OTEL_TRACES_SAMPLER_ARG` ([c20e7c7](c20e7c77bb2da30737f78a48e4a513d6f3117f24)) - ✨ add a axum layer for gRPC (#36) ([bf7daee](bf7daeeebe1ffd07834388c81349ab7a972abdbe)) - ✨ log under target `otel::setup` detected configuration by otel setup tools ([6c2f5c1](6c2f5c119bea731cb3de770dabcfe726c8edc227)) - ✨ provide opinionated `tracing_subscriber_ext` ([53963eb](53963eb1ee543f3b1c0a0a90a9c00a319694f71b)) ### Changed - 📝 add sample to overwrite `otel.name` ([1dae1aa](1dae1aab7edb2b1cc793ab1e609ea6153e73f2d3)) - 📝 update changelog ([2945358](29453580794da60dacf449676820a8731fd036e9)) ## [0.9.0] - 2023-02-05 ### Added - ✨ add `DetectResource` builder to help detection for [Resource Semantic Conventions | OpenTelemetry](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#semantic-attributes-with-sdk-provided-default-value) ([db7552e](db7552efdc5ea842bc17e19604a46ebe77283d0c)) ### Changed - 📝 add instruction to launch jaeger for local dev ([95411e9](95411e9640fc7a1e4eaf7bc5a6d9d07362cfe752)) - 📝 improve sample ([1b91fbf](1b91fbf9172578a81598292666fa9bd854a7f4c5)) ### Fixed - 🐛 fix mega-linter.yml ([6494dd6](6494dd6dab76f09e3364b1fe508ee6edae39356b)) ## [0.8.2] - 2023-01-30 ### Fixed - 🐛 restore missing line in changelog ([f46c342](f46c3427fa6f31e8aa4e550315160ce2bcbafb1b)) - 🐛 use correct env variable (OTEL_PROPAGATORS) when setting up propagators ([c2d34eb](c2d34eb54a7672b62aefd3429cc264235c5f952d)) ## [0.8.1] - 2023-01-29 ### Added - ✨ add `init_propagator` based on OTEL_PROPAGATORS ([b45b2f3](b45b2f3a39afaf86a589b5cef01e147f11416c3d)) ### Changed - 📝 update documentation & samples about configuration ([75a040d](75a040d08ebd41901a803562b1b8788f9a38e031)) ## [0.7.1] - 2023-01-01 ### Changed - 📝 use more OTEL env variable into sample ([048f57c](048f57c668a352739172ffd3af965f263452a4a2)) ## [0.7.0] - 2022-12-28 ### Added - ✨ add a layer`response_with_trace_layer` to have `traceparent` injected into response ([368c59d](368c59d0b0a928459b24e21ff26eac337c79a283)) ### Changed - 📝 add compatibility matrix ([9312737](93127375a8393a4b8df9dddbd108f875c1ab9cee)) - 📝 update changelog ([820ae63](820ae63eedcfaa76d5182c9c628c233a007ce8e0)) ## [0.5.2] - 2022-11-06 ### Fixed - Fix: do not populate http.route when not supported by the HTTP server framework ([93cedaa](93cedaa7a94904cff127a966db94b70dd697cc6a)) ## [0.5.1] - 2022-11-01 ### Added - :green_heart: add protoc into the CI require by `opentelemetry-proto` ([a1777c6](a1777c631c603074f9928787d156a5d4806bc708)) ### Removed - :rotating_light: remove useless code (after validation that experiment is ok) ([b17d9f0](b17d9f0a54a76ea3b185acbcdb97bfd0efffca98)) ## [0.3.0] - 2022-08-04 ### Added - :pencil: add a sample about how to retrieve trace_id ([6dd26ff](6dd26ff288f95b719a42c4c1939596532a3f9e4c)) ### Removed - :heavy_minus_sign: remove unused tansitive dependencies ([bca0c14](bca0c1485ac47ee2756f5dc7963863b2f6d39057)) ## [0.2.1] - 2022-06-11 ### Added - :sparkles: add code for opentelemetry_tracing_layer ([9403583](94035838f97aa61ad304791f0f3174e042a566f3)) - :sparkles: add tools to init tracer and find trace_id ([acb52a3](acb52a3ed98aeed7733ce3dba7aba894122a8949)) - :pencil: add examples code ([0482b59](0482b59f19af6d0e74082b43c19783e1d08e4c95)) - :pencil: add missing info for release ([a2f7c09](a2f7c0961366bcfd160ee89360d30c8874cfb6fd)) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to tracing-opentelemetry-instrumentation-sdk Thank you for your interest in contributing! This document provides guidelines and instructions for setting up your development environment and contributing to the project. ## Development Environment Setup ### Prerequisites 1. **Install mise** (if not already installed): ```bash # Using the install script curl https://mise.run | sh # Or using a package manager # macOS: brew install mise # Arch: pacman -S mise # Ubuntu/Debian: see https://mise.jdx.dev/installing-mise.html ``` 2. **Clone the repository**: ```bash git clone https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk.git cd tracing-opentelemetry-instrumentation-sdk ``` 3. **Install tools and dependencies**: ```bash # Install all tools defined in .mise.toml (Rust, protoc, grpcurl, etc.) mise install # Activate the environment (or use 'mise use' for shell integration) mise activate ``` ### Available Development Tasks We use `mise` tasks for all development workflows. Here are the main tasks: #### Code Quality & Formatting ```bash # Format code mise run format # Lint code (clippy + format check) mise run lint # Check code with all feature combinations mise run check # Run cargo deny security checks mise run deny ``` #### Testing ```bash # Run tests (using nextest + doctests) mise run test # Test each feature separately (slower but thorough) mise run test-each-feature ``` #### Tool Installation These tasks automatically install required tools if not present: ```bash # Individual tool installation (usually handled automatically by other tasks) mise run install:cargo-hack mise run install:cargo-nextest mise run install:cargo-insta mise run install:cargo-deny ``` #### Container & Examples ```bash # Start Jaeger all-in-one for local development mise run run-jaeger # Run example applications mise run run-example-grpc-server # gRPC server example mise run run-example-grpc-client # gRPC client example mise run run-example-axum-otlp-server # Axum HTTP server mise run run-example-http-client # HTTP client test mise run run-example-load # Load testing example ``` #### Version Management ```bash # Set version across all workspace crates mise run set-version 0.1.0 ``` ### Development Workflow 1. **Setup environment** (first time only): ```bash mise install ``` 2. **Before making changes**: ```bash # Format and check code mise run format mise run lint mise run check ``` 3. **While developing**: ```bash # Run tests frequently mise run test # Test with examples if relevant mise run run-jaeger & # Start Jaeger in background mise run run-example-axum-otlp-server ``` 4. **Before submitting PR**: ```bash # Full validation mise run format mise run lint mise run check mise run test mise run deny ``` ### Testing with OpenTelemetry #### Local Jaeger Setup ```bash # Start Jaeger (runs on various ports including 16686 for UI, 4317/4318 for OTLP) mise run run-jaeger # Open Jaeger UI open http://localhost:16686 ``` #### Running Examples ```bash # Terminal 1: Start Jaeger mise run run-jaeger # Terminal 2: Start example server mise run run-example-axum-otlp-server # Terminal 3: Send requests and check traces mise run run-example-http-client # Then check traces in Jaeger UI at http://localhost:16686 ``` ### Project Structure This workspace contains several crates: - **`init-tracing-opentelemetry/`**: Helpers to initialize tracing + opentelemetry - **`axum-tracing-opentelemetry/`**: Axum middlewares for tracing integration - **`tonic-tracing-opentelemetry/`**: Tonic (gRPC) middlewares for tracing - **`tracing-opentelemetry-instrumentation-sdk/`**: Core instrumentation SDK - **`fake-opentelemetry-collector/`**: Testing utilities and fake collector - **`testing-tracing-opentelemetry/`**: Test utilities - **`examples/`**: Working examples demonstrating usage ### Code Style & Guidelines - **Formatting**: We use `rustfmt` with default settings - **Linting**: All clippy warnings must be addressed - **Testing**: Add tests for new functionality - **Documentation**: Update documentation for public APIs - **Examples**: Update examples if adding new features ### Continuous Integration Our CI pipeline runs: - `mise run check` - Feature compatibility checks - `mise run lint` - Code formatting and clippy - `mise run test` - Full test suite - `mise run deny` - Security and license checks Make sure all these pass locally before submitting a PR. ### Common Issues & Solutions #### Tool Installation Issues If tools fail to install automatically: ```bash # Install cargo-binstall manually curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash # Then retry the task mise run ``` #### Container Runtime The project supports multiple container runtimes (podman, nerdctl, docker). If you have issues: ```bash # Make sure one of these is installed and available which podman || which nerdctl || which docker ``` #### Environment Variables Key environment variables (already set in `.mise.toml`): - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://127.0.0.1:4317"` - `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="grpc"` - `OTEL_TRACES_SAMPLER="always_on"` ### Getting Help - Check existing [issues](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/issues) - Review the [examples/](examples/) directory for usage patterns - Look at test files for API usage examples - Open a new issue for bugs or feature requests ### Submitting Changes 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/my-new-feature` 3. Make your changes following the guidelines above 4. Run the full test suite: `mise run lint && mise run test && mise run deny` 5. Commit with descriptive messages 6. Push to your fork and submit a pull request Thank you for contributing! ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "axum-tracing-opentelemetry", "examples/*", "fake-opentelemetry-collector", "init-tracing-opentelemetry", "testing-tracing-opentelemetry", "tonic-tracing-opentelemetry", "tracing-opentelemetry-instrumentation-sdk", ] exclude = ["target"] [workspace.package] version = "0.32.0" edition = "2024" rust-version = "1.91.0" homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk" repository = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk" license = "CC0-1.0" [workspace.dependencies] assert2 = "0.4" axum = { version = "0.8", default-features = false } http = "^1" hyper = "1" insta = { version = "^1", features = ["redactions", "yaml"] } opentelemetry = { version = "0.31", default-features = false, features = [ "trace", ] } opentelemetry-aws = { version = "0.19", default-features = false } opentelemetry-jaeger-propagator = { version = "0.31", default-features = false } opentelemetry-otlp = { version = "0.31", default-features = false } opentelemetry-proto = { version = "0.31", default-features = false } opentelemetry-resource-detectors = { version = "0.10", default-features = false } opentelemetry_sdk = { version = "0.31", default-features = false, features = [ "rt-tokio", ] } opentelemetry-semantic-conventions = { version = "0.31", default-features = false } opentelemetry-stdout = { version = "0.31" } opentelemetry-zipkin = { version = "0.31", default-features = false } rstest = "0.26" tokio = { version = "1", default-features = false } tokio-stream = { version = "0.1", default-features = false } tonic = { version = "0.14", default-features = false } # should be sync with opentelemetry-proto tower = { version = "0.5", default-features = false } tracing = "0.1" tracing-opentelemetry = "0.32" [workspace.metadata.release] pre-release-commit-message = "🚀 (cargo-release) version {{version}}" tag-prefix = "" tag-name = "{{prefix}}{{version}}" tag-message = "🔖 {{version}}" [profile.dev.package.insta] opt-level = 3 [profile.dev.package.similar] opt-level = 3 ================================================ FILE: LICENSE ================================================ Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. ================================================ FILE: README.md ================================================ # tracing-opentelemetry-instrumentation-sdk A set of rust crates to help working with tracing + opentelemetry - `init-tracing-opentelemetry`: A set of helpers to initialize (and more) tracing + opentelemetry (compose your own or use opinionated preset) - `axum-tracing-opentelemetry`: Middlewares and tools to integrate axum + tracing + opentelemetry. - `fake-opentelemetry-collector`: A Fake (basic) opentelemetry collector, useful to test what is collected opentelemetry ## For local dev / demo To collect and visualize trace on local, some ofthe simplest solutions: ![screenshot](examples/axum-otlp/Screenshot-20251103_1308.jpg) ### Otel Desktop Viewer [CtrlSpice/otel-desktop-viewer: desktop-collector](https://github.com/CtrlSpice/otel-desktop-viewer) ```sh # also available via `brew install --cask ctrlspice/tap/otel-desktop-viewer` # For AMD64 (most common) # docker/nerdctl/podman or any container runner docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 ghcr.io/ctrlspice/otel-desktop-viewer:latest-amd64 open http://localhost:8000 ``` ### Jaeger all-in-one ```sh # launch Jaeger with OpenTelemetry, Jaeger, Zipking,... mode. # see https://www.jaegertracing.io/docs/1.49/getting-started/#all-in-one # docker/nerdctl/podman or any container runner docker run --rm --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 5778:5778 \ -p 9411:9411 \ cr.jaegertracing.io/jaegertracing/jaeger:latest open http://localhost:16686 ``` Then : - setup env variable (or not), (eg see [.envrc](.envrc)) - launch your server - send the request - copy trace_id from log (or response header) - paste into Jaeger web UI ## To release Use the github workflow `release-plz`. ================================================ FILE: axum-tracing-opentelemetry/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.30.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.29.0...axum-tracing-opentelemetry-v0.30.0) - 2025-08-25 ### Added - *(axum)* optional extraction of `client.address` (former `client_ip`) from http headers or socket's info # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.33.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/releases/tag/axum-tracing-opentelemetry-v0.33.0) - 2026-01-19 ### Added - *(deps)* update to rust 1.87 & edition 2024 ([#317](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/317)) ## [0.33.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.32.3...axum-tracing-opentelemetry-v0.33.0) - 2026-01-19 ### Added - *(deps)* update to rust 1.87 & edition 2024 ([#317](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/317)) ## [0.32.2](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.32.1...axum-tracing-opentelemetry-v0.32.2) - 2025-11-03 ### Changed - update sample ## [0.32.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.32.0...axum-tracing-opentelemetry-v0.32.1) - 2025-10-14 ### Wip - use `opentelemetry-semantic-conventions` instead of `static &str` ## [0.30.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.30.0...axum-tracing-opentelemetry-v0.30.1) - 2025-09-27 ## [0.28.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.28.0...axum-tracing-opentelemetry-v0.28.1) - 2025-06-03 ### Removed - remove deprecated sample from README ## [0.26.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.26.0...axum-tracing-opentelemetry-v0.26.1) - 2025-02-26 ### Removed - *(deps)* remove minor constraint when major > 1 ## [0.25.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.24.2...axum-tracing-opentelemetry-v0.25.0) - 2025-01-02 ### Added - *(deps)* update rust crate axum to 0.8 (#197) ## [0.24.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.24.0...axum-tracing-opentelemetry-v0.24.1) - 2024-11-24 ### Fixed - Use guard pattern to allow consumers to ensure final trace is sent ([#185](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/185)) ## [0.21.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.19.0...axum-tracing-opentelemetry-v0.21.0) - 2024-08-31 ### Fixed - 🐛 workaround for a delay, batch,... behavior in otlp exporter and test with fake-opentelemetry-collector (closed too early) - 🐛 fix build of contributions (upgrade of opentelemetry, fake collector for logs,...) - 🐛 Re-export tracing_level_info feature from axum to sdk ([#147](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/147)) ### Changed - 💄 update deprecated syntax "default_features" in Cargo.toml - ⬆️ upgrade to rstest 0.22 ## [0.17.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/axum-tracing-opentelemetry-v0.17.0...axum-tracing-opentelemetry-v0.17.1) - 2024-02-24 ### Other - 👷 tune release-plz - ✏️ Fix broken /examples URLs ([#129](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/129)) ================================================ FILE: axum-tracing-opentelemetry/Cargo.toml ================================================ [package] name = "axum-tracing-opentelemetry" description = "Middlewares and tools to integrate axum + tracing + opentelemetry" readme = "README.md" keywords = ["axum", "tracing", "opentelemetry"] categories = [ "development-tools::debugging", "development-tools::profiling", "web-programming", ] homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/axum-tracing-opentelemetry" rust-version.workspace = true edition.workspace = true version = "0.33.1" repository.workspace = true license.workspace = true [dependencies] axum = { workspace = true, features = ["matched-path", "tokio"] } futures-core = "0.3" futures-util = { version = "0.3", default-features = false, features = [] } http = { workspace = true } opentelemetry = { workspace = true, features = [ "trace", ], default-features = false } opentelemetry-semantic-conventions = { workspace = true } pin-project-lite = "0.2" tower = { workspace = true } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../tracing-opentelemetry-instrumentation-sdk", features = [ "http", ], version = "0.32" } [dev-dependencies] fake-opentelemetry-collector = { path = "../fake-opentelemetry-collector" } testing-tracing-opentelemetry = { path = "../testing-tracing-opentelemetry" } assert2 = { workspace = true } hyper = { workspace = true } insta = { workspace = true } opentelemetry-otlp = { workspace = true, features = [ "http-proto", "reqwest-client", "reqwest-rustls", ] } opentelemetry-proto = { workspace = true, features = ["gen-tonic"] } rstest = { workspace = true } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true, features = ["net"] } tracing-subscriber = { version = "0.3", default-features = false, features = [ "env-filter", "fmt", "json", ] } # need tokio runtime to run smoke tests. opentelemetry_sdk = { workspace = true, features = [ "trace", "rt-tokio", "testing", ] } [features] # to use level `info` instead of `trace` to create otel span tracing_level_info = [ "tracing-opentelemetry-instrumentation-sdk/tracing_level_info", ] ================================================ FILE: axum-tracing-opentelemetry/README.md ================================================ # axum-tracing-opentelemetry [![crates license](https://img.shields.io/crates/l/axum-tracing-opentelemetry.svg)](http://creativecommons.org/publicdomain/zero/1.0/) [![crate version](https://img.shields.io/crates/v/axum-tracing-opentelemetry.svg)](https://crates.io/crates/axum-tracing-opentelemetry) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) Middlewares to integrate axum + tracing + opentelemetry. - Read OpenTelemetry header from incoming request - Start a new trace if no trace found in the incoming request - Trace is attached into tracing'span - OpenTelemetry Span is created on close of the tracing's span (behavior from [tracing-opentelemetry]) For examples, you can look at the [examples](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/examples/) folder. ```txt //... use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; #[tokio::main] async fn main() -> Result<(), axum::BoxError> { // very opinionated init of tracing, look as is source to make your own let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; let app = app(); // run it let addr = &"0.0.0.0:3000".parse::()?; tracing::warn!("listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app.into_make_service()).await?; Ok(()) } fn app() -> Router { Router::new() .route("/", get(index)) // request processed inside span // include trace context as header into the response .layer(OtelInResponseLayer::default()) //start OpenTelemetry trace on incoming request .layer(OtelAxumLayer::default()) .route("/health", get(health)) // request processed without span / trace } ``` For more info about how to initialize, you can look at crate [`init-tracing-opentelemetry`] or [`tracing-opentelemetry`]. ![screenshot](../examples/axum-otlp/Screenshot-20251103_1308.jpg) ## Changelog - History [CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md) [`tracing-opentelemetry`]: https://crates.io/crates/tracing-opentelemetry [`init-tracing-opentelemetry`]: https://crates.io/crates/init-tracing-opentelemetry ================================================ FILE: axum-tracing-opentelemetry/src/lib.rs ================================================ //#![warn(missing_docs)] #![forbid(unsafe_code)] #![warn(clippy::perf)] #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![doc = include_str!("../README.md")] #[allow(deprecated)] pub mod middleware; /// for basic backward compatibility and transition #[allow(deprecated)] pub use self::middleware::opentelemetry_tracing_layer; /// for basic backward compatibility and transition #[allow(deprecated)] pub use self::middleware::response_with_trace_layer; // reexport tracing_opentelemetry_instrumentation_sdk crate pub use tracing_opentelemetry_instrumentation_sdk; ================================================ FILE: axum-tracing-opentelemetry/src/middleware/mod.rs ================================================ mod response_injector; mod trace_extractor; pub use response_injector::*; pub use trace_extractor::*; ================================================ FILE: axum-tracing-opentelemetry/src/middleware/response_injector.rs ================================================ use futures_core::future::BoxFuture; use http::{Request, Response}; use std::task::{Context, Poll}; use tower::{Layer, Service}; use tracing_opentelemetry_instrumentation_sdk as otel; use tracing_opentelemetry_instrumentation_sdk::http as otel_http; #[deprecated( since = "0.12.0", note = "keep for transition, replaced by OtelInResponseLayer" )] #[must_use] pub fn response_with_trace_layer() -> OtelInResponseLayer { OtelInResponseLayer {} } #[derive(Default, Debug, Clone)] pub struct OtelInResponseLayer; impl Layer for OtelInResponseLayer { type Service = OtelInResponseService; fn layer(&self, inner: S) -> Self::Service { OtelInResponseService { inner } } } #[derive(Default, Debug, Clone)] pub struct OtelInResponseService { inner: S, } impl Service> for OtelInResponseService where S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; // `BoxFuture` is a type alias for `Pin>` type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } #[allow(unused_mut)] fn call(&mut self, mut request: Request) -> Self::Future { let future = self.inner.call(request); Box::pin(async move { let mut response = future.await?; // inject the trace context into the response (optional but useful for debugging and client) otel_http::inject_context(&otel::find_current_context(), response.headers_mut()); Ok(response) }) } } ================================================ FILE: axum-tracing-opentelemetry/src/middleware/trace_extractor.rs ================================================ // //! `OpenTelemetry` tracing middleware. //! //! This returns a [`OtelAxumLayer`] configured to use [`OpenTelemetry`'s conventional span field //! names][otel]. //! //! # Span fields //! //! Try to provide some of the field define at //! [semantic-conventions/.../http-spans.md](https://github.com/open-telemetry/semantic-conventions/blob/v1.25.0/docs/http/http-spans.md) //! (Please report or provide fix for missing one) //! //! # Example //! //! ``` //! use axum::{Router, routing::get, http::Request}; //! use axum_tracing_opentelemetry::middleware::OtelAxumLayer; //! use std::net::SocketAddr; //! use tower::ServiceBuilder; //! //! let app = Router::new() //! .route("/", get(|| async {})) //! .layer(OtelAxumLayer::default()); //! //! # async { //! let addr = &"0.0.0.0:3000".parse::().unwrap(); //! let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); //! axum::serve(listener, app.into_make_service()) //! .await //! .expect("server failed"); //! # }; //! ``` //! use axum::extract::{ConnectInfo, MatchedPath}; use http::{Request, Response}; use opentelemetry_semantic_conventions::attribute::{CLIENT_ADDRESS, HTTP_ROUTE}; use pin_project_lite::pin_project; use std::{ error::Error, future::Future, net::SocketAddr, pin::Pin, task::{Context, Poll}, }; use tower::{Layer, Service}; use tracing::Span; use tracing_opentelemetry_instrumentation_sdk::http::{ self as otel_http, extract_client_ip_from_headers, }; #[deprecated( since = "0.12.0", note = "keep for transition, replaced by OtelAxumLayer" )] #[must_use] pub fn opentelemetry_tracing_layer() -> OtelAxumLayer { OtelAxumLayer::default() } pub type Filter = fn(&str) -> bool; /// layer/middleware for axum: /// /// - propagate `OpenTelemetry` context (`trace_id`,...) to server /// - create a Span for `OpenTelemetry` (and tracing) on call /// /// `OpenTelemetry` context are extracted from tracing's span. #[derive(Default, Debug, Clone)] pub struct OtelAxumLayer { filter: Option, try_extract_client_ip: bool, } // add a builder like api impl OtelAxumLayer { #[must_use] pub fn filter(self, filter: Filter) -> Self { let mut me = self; me.filter = Some(filter); me } /// Enable or disable (default) the extraction of client's ip. /// Extraction from (in order): /// /// 1. http header 'Forwarded' /// 2. http header `X-Forwarded-For` /// 3. socket connection ip, use the `axum::extract::ConnectionInfo` (see [`Router::into_make_service_with_connect_info`] for more details) /// 4. empty (failed to extract the information) /// /// The extracted value could an ip v4, ip v6, a string (as `Forwarded` can use label or hide the client). /// The extracted value is stored it as `client.address` in the span/trace /// /// [`Router::into_make_service_with_connect_info`]: axum::routing::Router::into_make_service_with_connect_info #[must_use] pub fn try_extract_client_ip(self, enable: bool) -> Self { let mut me = self; me.try_extract_client_ip = enable; me } } impl Layer for OtelAxumLayer { /// The wrapped service type Service = OtelAxumService; fn layer(&self, inner: S) -> Self::Service { OtelAxumService { inner, filter: self.filter, try_extract_client_ip: self.try_extract_client_ip, } } } #[derive(Debug, Clone)] pub struct OtelAxumService { inner: S, filter: Option, try_extract_client_ip: bool, } impl Service> for OtelAxumService where S: Service, Response = Response> + Clone + Send + 'static, S::Error: Error + 'static, //fmt::Display + 'static, S::Future: Send + 'static, B: Send + 'static, { type Response = S::Response; type Error = S::Error; // #[allow(clippy::type_complexity)] // type Future = futures_core::future::BoxFuture<'static, Result>; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { use tracing_opentelemetry::OpenTelemetrySpanExt; let req = req; let span = if self.filter.is_none_or(|f| f(req.uri().path())) { let route = http_route(&req); let method = req.method(); let client_ip = if self.try_extract_client_ip { extract_client_ip_from_headers(req.headers()) .map(ToString::to_string) .or_else(|| { req.extensions() .get::>() .map(|ConnectInfo(client_ip)| client_ip.to_string()) }) } else { None }; let span = otel_http::http_server::make_span_from_request(&req); span.record(HTTP_ROUTE, route); span.record("otel.name", format!("{method} {route}").trim()); if let Some(client_ip) = client_ip { span.record(CLIENT_ADDRESS, client_ip); } if let Err(error) = span.set_parent(otel_http::extract_context(req.headers())) { tracing::warn!(?error, "can not set parent trace_id to span"); } span } else { tracing::Span::none() }; let future = { let _enter = span.enter(); self.inner.call(req) }; ResponseFuture { inner: future, span, } } } pin_project! { /// Response future for [`Trace`]. /// /// [`Trace`]: super::Trace pub struct ResponseFuture { #[pin] pub(crate) inner: F, pub(crate) span: Span, // pub(crate) start: Instant, } } impl Future for ResponseFuture where Fut: Future, E>>, E: std::error::Error + 'static, { type Output = Result, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let _guard = this.span.enter(); let result = futures_util::ready!(this.inner.poll(cx)); otel_http::http_server::update_span_from_response_or_error(this.span, &result); Poll::Ready(result) } } #[inline] fn http_route(req: &Request) -> &str { req.extensions() .get::() .map_or_else(|| "", |mp| mp.as_str()) } #[cfg(test)] mod tests { use super::*; use axum::{Router, body::Body, routing::get}; use http::{Request, StatusCode}; use rstest::rstest; use testing_tracing_opentelemetry::{FakeEnvironment, assert_trace}; use tower::Service; #[rstest] #[case("filled_http_route_for_existing_route", "http://example.com/users/123", &[], false)] #[case("empty_http_route_for_nonexisting_route", "/idontexist/123", &[], false)] #[case("status_code_on_close_for_ok", "/users/123", &[], false)] #[case("status_code_on_close_for_error", "/status/500", &[], false)] #[case("filled_http_headers", "/users/123", &[("user-agent", "tests"), ("x-forwarded-for", "127.0.0.1")], false)] #[case("call_with_w3c_trace", "/users/123", &[("traceparent", "00-b2611246a58fd7ea623d2264c5a1e226-b2c9b811f2f424af-01")], true)] #[case("trace_id_in_child_span", "/with_child_span", &[], false)] #[case("trace_id_in_child_span_for_remote", "/with_child_span", &[("traceparent", "00-b2611246a58fd7ea623d2264c5a1e226-b2c9b811f2f424af-01")], true)] // failed to extract "http.route" before axum-0.6.15 // - https://github.com/davidB/axum-tracing-opentelemetry/pull/54 (reverted) // - https://github.com/tokio-rs/axum/issues/1441#issuecomment-1272158039 #[case("extract_route_from_nested", "/nest/123", &[], false)] #[tokio::test(flavor = "multi_thread")] async fn check_span_event( #[case] name: &str, #[case] uri: &str, #[case] headers: &[(&str, &str)], #[case] is_trace_id_constant: bool, ) { let mut fake_env = FakeEnvironment::setup().await; { let mut svc = Router::new() .route("/users/{id}", get(|| async { StatusCode::OK })) .route( "/status/500", get(|| async { StatusCode::INTERNAL_SERVER_ERROR }), ) .route( "/with_child_span", get(|| async { let span = tracing::span!(tracing::Level::INFO, "my child span"); span.in_scope(|| { // Any trace events in this closure or code called by it will occur within // the span. }); StatusCode::OK }), ) .nest( "/nest", Router::new() .route("/{nest_id}", get(|| async {})) .fallback(|| async { (StatusCode::NOT_FOUND, "inner fallback") }), ) .fallback(|| async { (StatusCode::NOT_FOUND, "outer fallback") }) .layer(opentelemetry_tracing_layer()); let mut builder = Request::builder(); for (key, value) in headers { builder = builder.header(*key, *value); } let req = builder.uri(uri).body(Body::empty()).unwrap(); let _res = svc.call(req).await.unwrap(); // while res.data().await.is_some() {} // res.trailers().await.unwrap(); // drop(res); } let (tracing_events, otel_spans) = fake_env.collect_traces().await; assert_trace(name, tracing_events, otel_spans, is_trace_id_constant); } } ================================================ FILE: cliff.toml ================================================ # git-cliff ~ default configuration file # https://git-cliff.org/docs/configuration # # Lines starting with "#" are comments. # Configuration options are organized into tables and keys. # See documentation for more information on available options. # see https://github.com/orhun/git-cliff/blob/main/examples/keepachangelog.toml [changelog] # changelog header header = """ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). """ # template for the changelog body # https://keats.github.io/tera/docs/#introduction body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [Unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} - {{ commit.message | split(pat="\\n") | first | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\ {% endfor %} {% endfor %}\n """ # remove the leading and trailing whitespace from the template trim = true # changelog footer footer = """ """ # postprocessors postprocessors = [ # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL ] [git] # parse the commits based on https://www.conventionalcommits.org conventional_commits = false # filter out the commits that are not conventional filter_unconventional = false # process each line of a commit as an individual commit split_commits = false # regex for preprocessing the commit messages commit_preprocessors = [ # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers ] # regex for parsing and grouping commits # try to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) commit_parsers = [ { message = "^(feat|✨|💥)", group = "Added" }, { message = "^(fix|🐛|🚑️|👽️)", group = "Fixed" }, { message = "^(doc|✏️|📝)", group = "Changed" }, { message = "^(perf|⚡️)", group = "Changed" }, { message = "^(refactor|🎨|🔥|♻️)", group = "Refactor", skip = true }, { message = "^(style|💄)", group = "Changed" }, { message = "^(test|✅)", group = "Fixed", skip = true }, { message = "^(chore\\(release\\): prepare for|🔖|🚀)", skip = true }, { message = "^(chore\\(deps\\)|⬇️|⬆️|➕|➖)", group = "Changed" }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, { message = "^(chore|ci|💚|👷|🚧)", group = "Changed", skip = true }, { message = "^(🔒️|🔐)", group = "Security" }, { body = ".*security", group = "Security" }, { message = "^revert", group = "Changed" }, { message = "^.*: add", group = "Added" }, { message = "^.*: support", group = "Added" }, { message = "^.*: remove", group = "Removed" }, { message = "^.*: delete", group = "Removed" }, ] # protect breaking changes from being skipped due to matching a skipping commit_parser protect_breaking_commits = false # filter out the commits that are not matched by commit parsers filter_commits = true # regex for matching git tags tag_pattern = "[0-9]+\\.[0-9]+\\.[0-9]+.*" # regex for skipping tags skip_tags = "" # regex for ignoring tags ignore_tags = "" # sort the tags topologically topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" # limit the number of commits included in the changelog. # limit_commits = 42 ================================================ FILE: deny.toml ================================================ # This template contains all of the possible sections and their default values # Note that all fields that take a lint level have these possible values: # * deny - An error will be produced and the check will fail # * warn - A warning will be produced, but the check will not fail # * allow - No warning or error will be produced, though in some cases a note # will be # The values provided in this template are the default values that will be used # when any section or field is not specified in your own configuration # Root options # The graph table configures how the dependency graph is constructed and thus # which crates the checks are performed against [graph] # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific # dependency, such as, for example, the `nix` crate only being used via the # `target_family = "unix"` configuration, that only having windows targets in # this list would mean the nix crate, as well as any of its exclusive # dependencies not shared by any other crates, would be ignored, as the target # list here is effectively saying which targets you are building for. targets = [ # The triple can be any string, but only the target triples built in to # rustc (as of 1.40) can be checked against actual config expressions # "x86_64-unknown-linux-musl", # You can also specify which target_features you promise are enabled for a # particular target. target_features are currently not validated against # the actual valid features supported by the target architecture. # { triple = "wasm32-unknown-unknown", features = ["atomics"] }, ] # When creating the dependency graph used as the source of truth when checks are # executed, this field can be used to prune crates from the graph, removing them # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate # is pruned from the graph, all of its dependencies will also be pruned unless # they are connected to another crate in the graph that hasn't been pruned, # so it should be used with care. The identifiers are [Package ID Specifications] # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) # exclude = [] # If true, metadata will be collected with `--all-features`. Note that this can't # be toggled off if true, if you want to conditionally enable `--all-features` it # is recommended to pass `--all-features` on the cmd line instead all-features = false # If true, metadata will be collected with `--no-default-features`. The same # caveat with `all-features` applies no-default-features = false # If set, these feature will be enabled when collecting metadata. If `--features` # is specified on the cmd line they will take precedence over this option. # features = [] # The output table provides options for how/if diagnostics are outputted [output] # When outputting inclusion graphs in diagnostics that include features, this # option can be used to specify the depth at which feature edges will be added. # This option is included since the graphs can be quite large and the addition # of features from the crate(s) to all of the graph roots can be far too verbose. # This option can be overridden via `--feature-depth` on the cmd line feature-depth = 1 # This section is considered when running `cargo deny check advisories` # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] # The path where the advisory databases are cloned/fetched into # db-path = "$CARGO_HOME/advisory-dbs" # The url(s) of the advisory databases to use # db-urls = ["https://github.com/rustsec/advisory-db"] # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ # "RUSTSEC-0000-0000", # { id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, # "a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish # { crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. # See Git Authentication for more information about setting up git authentication. # git-fetch-with-cli = true # This section is considered when running `cargo deny check licenses` # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0", "ISC", "MIT", # "OpenSSL", "Unicode-3.0", # "Unicode-DFS-2016", "Unlicense", "Zlib" ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. # [possible values: any between 0.0 and 1.0]. confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list # { allow = ["Zlib"], crate = "adler32" }, ] # Some crates don't have (easily) machine readable licensing information, # adding a clarification entry for it allows you to manually specify the # licensing information [[licenses.clarify]] # The package spec the clarification applies to crate = "ring" # The SPDX expression for the license requirements of the crate expression = "MIT AND ISC AND OpenSSL" license-files = [ # Each entry is a crate relative path, and the (opaque) hash of its contents { path = "LICENSE", hash = 0xbd0eed23 }, ] [licenses.private] # One or more files in the crate's source used as the "source of truth" for # the license expression. If the contents match, the clarification will be used # when running the license check, otherwise the clarification will be ignored # and the crate will be checked normally, which may produce warnings or errors # depending on the rest of your configuration # If true, ignores workspace crates that aren't published, or are only # published to private registries. # To see how to mark a crate as unpublished (to the official registry), # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. ignore = false # One or more private registries that you might publish crates to, if a crate # is only published to private registries, and ignore is true, the crate will # not have its license(s) checked registries = [ # "https://sekretz.com/registry ] # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected multiple-versions = "deny" # Lint level for when a crate version requirement is `*` wildcards = "allow" # The graph highlighting used when creating dotgraphs for crates # with multiple versions # * lowest-version - The path to the lowest versioned duplicate is highlighted # * simplest-path - The path to the version with the fewest edges is highlighted # * all - Both lowest-version and simplest-path are used highlight = "all" # The default lint level for `default` features for crates that are members of # the workspace that is being checked. This can be overridden by allowing/denying # `default` on a crate-by-crate basis if desired. workspace-default-features = "allow" # The default lint level for `default` features for external crates that are not # members of the workspace. This can be overridden by allowing/denying `default` # on a crate-by-crate basis if desired. external-default-features = "allow" # List of crates that are allowed. Use with care! allow = [ # "ansi_term@0.11.0", # { crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, ] # List of crates to deny deny = [ # "ansi_term@0.11.0", # { crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate # { crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, ] # List of features to allow/deny # Each entry the name of a crate and a version range. If version is # not specified, all versions will be matched. # [[bans.features]] # crate = "reqwest" # Features to not allow # deny = ["json"] # Features to allow # allow = [ # "rustls", # "__rustls", # "__tls", # "hyper-rustls", # "rustls", # "rustls-pemfile", # "rustls-tls-webpki-roots", # "tokio-rustls", # "webpki-roots", # ] # If true, the allowed features must exactly match the enabled feature set. If # this is set there is no point setting `deny` # exact = true # Certain crates/versions that will be skipped when doing duplicate detection. skip = [ # "ansi_term@0.11.0", # { crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, # "axum@0.7", # tonic depend on axum 0.7 # "axum-core@0.4", # tonic depend on axum 0.7 # "matchit@0.7", # tonic depend on axum 0.7 # "tower@0.4", # axum 0.7 use tower 0.5, but hyper still use 0.4 # "sync_wrapper", # axum direct and transive dependency use multiple version # "regex-syntax", # "regex-automata", # "indexmap", # "hermit-abi", # "rustls-native-certs", "hashbrown", "getrandom", # "rand", # until tonic & tower upgrade # "rand_chacha", # until tonic & tower upgrade # "rand_core", # until tonic & tower upgrade "r-efi", # "socket2", # "wasi", "wit-bindgen", ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive # dependencies starting at the specified crate, up to a certain depth, which is # by default infinite. skip-tree = [ # "ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies # { crate = "ansi_term@0.11.0", depth = 20 }, "windows-targets", "windows-sys", # "async-std", ] # This section is considered when running `cargo deny check sources`. # More documentation about the 'sources' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html [sources] # Lint level for what to happen when a crate from a crate registry that is not # in the allow list is encountered unknown-registry = "warn" # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered unknown-git = "warn" # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [] [sources.allow-org] # github.com organizations to allow git sources for github = [] # gitlab.com organizations to allow git sources for gitlab = [] # bitbucket.org organizations to allow git sources for bitbucket = [] ================================================ FILE: examples/axum-otlp/Cargo.toml ================================================ [package] name = "examples-axum-otlp" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] axum = { workspace = true, default-features = true } axum-tracing-opentelemetry = { path = "../../axum-tracing-opentelemetry" } init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "otlp", "tracing_subscriber_ext", "metrics" ] } opentelemetry = { workspace = true } opentelemetry-otlp = { workspace = true, default-features = false, features = [ "reqwest-rustls", "http-proto", "tls", ] } serde_json = "1" tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" } ================================================ FILE: examples/axum-otlp/README.md ================================================ # `examples-axum-otlp` In a terminal, run Configure the [environment variables](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/) for the OTLP exporter: ```sh # For GRPC: export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4317" export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="grpc" export OTEL_TRACES_SAMPLER="always_on" # For HTTP: export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://127.0.0.1:4318/v1/traces" export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf" export OTEL_TRACES_SAMPLER="always_on" ``` ```sh ❯ cd examples/axum-otlp ❯ cargo run Compiling examples-axum-otlp v0.1.0 (/home/david/src/github.com/davidB/axum-tracing-opentelemetry/examples/axum-otlp) Finished dev [unoptimized + debuginfo] target(s) in 3.60s Running `/home/david/src/github.com/davidB/axum-tracing-opentelemetry/target/debug/examples-axum-otlp` 0.000041809s INFO init_tracing_opentelemetry::tracing_subscriber_ext: init logging & tracing at init-tracing-opentelemetry/src/tracing_subscriber_ext.rs:82 on main 0.000221695s DEBUG otel::setup::resource: key: service.name, value: unknown_service at init-tracing-opentelemetry/src/resource.rs:63 on main 0.000242183s DEBUG otel::setup::resource: key: os.type, value: linux at init-tracing-opentelemetry/src/resource.rs:63 on main 0.000280946s DEBUG otel::setup: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://localhost:4317" at init-tracing-opentelemetry/src/otlp.rs:22 on main 0.000293128s DEBUG otel::setup: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "grpc" at init-tracing-opentelemetry/src/otlp.rs:23 on main 0.000377897s DEBUG otel::setup: OTEL_TRACES_SAMPLER: "always_on" at init-tracing-opentelemetry/src/otlp.rs:80 on main 0.000561931s DEBUG otel::setup: OTEL_PROPAGATORS: "tracecontext,baggage" at init-tracing-opentelemetry/src/lib.rs:97 on main 0.000134291s WARN examples_axum_otlp: listening on 0.0.0.0:3003 at examples/axum-otlp/src/main.rs:15 on main 0.000150401s INFO examples_axum_otlp: try to call `curl -i http://127.0.0.1:3003/` (with trace) at examples/axum-otlp/src/main.rs:16 on main 0.000159659s INFO examples_axum_otlp: try to call `curl -i http://127.0.0.1:3003/health` (with NO trace) at examples/axum-otlp/src/main.rs:17 on main ... ``` Into an other terminal, call the `/` (endpoint with `OtelAxumLayer` and `OtelInResponseLayer`) ```sh ❯ curl -i http://127.0.0.1:3003/ HTTP/1.1 200 OK content-type: application/json content-length: 50 traceparent: 00-b2611246a58fd7ea623d2264c5a1e226-b2c9b811f2f424af-01 tracestate: date: Wed, 28 Dec 2022 17:04:59 GMT {"my_trace_id":"b2611246a58fd7ea623d2264c5a1e226"} ``` call the `/health` (endpoint with NO layer) ```sh ❯ curl -i http://127.0.0.1:3003/health HTTP/1.1 200 OK content-type: application/json content-length: 15 date: Wed, 28 Dec 2022 17:14:07 GMT {"status":"UP"} ``` ================================================ FILE: examples/axum-otlp/src/main.rs ================================================ #![allow(clippy::let_with_type_underscore)] #![allow(clippy::default_constructed_unit_structs)] // warning since 1.71 use axum::extract::Path; use axum::{BoxError, Router, response::IntoResponse, routing::get}; use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; use serde_json::json; use std::net::SocketAddr; use tracing_opentelemetry_instrumentation_sdk::find_current_trace_id; #[tokio::main] async fn main() -> Result<(), BoxError> { // very opinionated init of tracing, look as is source to make your own let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; let app = app(); // run it let addr = &"0.0.0.0:3003".parse::()?; tracing::warn!("listening on {}", addr); tracing::info!("try to call `curl -i http://127.0.0.1:3003/` (with trace)"); //Devskim: ignore DS137138 tracing::info!("try to call `curl -i http://127.0.0.1:3003/health` (with NO trace)"); //Devskim: ignore DS137138 let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app.into_make_service()).await?; Ok(()) } fn app() -> Router { // build our application with a route Router::new() .route( "/proxy/{service}/{*path}", get(proxy_handler).post(proxy_handler), ) .route("/", get(index)) // request processed inside span // include trace context as header into the response .layer(OtelInResponseLayer::default()) //start OpenTelemetry trace on incoming request .layer(OtelAxumLayer::default()) .route("/health", get(health)) // request processed without span / trace } async fn health() -> impl IntoResponse { axum::Json(json!({ "status" : "UP" })) } #[tracing::instrument] async fn index() -> impl IntoResponse { tracing::info!(monotonic_counter.index = 1); sleep_10ms().await; sleep_10ms().await; sleep_10ms().await; let trace_id = find_current_trace_id(); dbg!(&trace_id); //std::thread::sleep(std::time::Duration::from_secs(1)); axum::Json(json!({ "my_trace_id": trace_id })) } #[tracing::instrument] async fn sleep_10ms() { tokio::time::sleep(std::time::Duration::from_millis(10)).await; } async fn proxy_handler(Path((service, path)): Path<(String, String)>) -> impl IntoResponse { // Overwrite the otel.name of the span tracing::Span::current().record("otel.name", format!("proxy {service}")); let trace_id = find_current_trace_id(); axum::Json( json!({ "my_trace_id": trace_id, "fake_proxy": { "service": service, "path": path } }), ) } ================================================ FILE: examples/bug_234_tls/Cargo.toml ================================================ [package] name = "bug_234_tls" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] axum = { workspace = true, default-features = true } axum-tracing-opentelemetry = { path = "../../axum-tracing-opentelemetry" } tokio = { version = "1", features = ["full"] } tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter", "std"] } init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "otlp", "tracing_subscriber_ext", "tls", ] } tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk", features = [ "http", ] } # opentelemetry = { version = "0.29.1", features = ["metrics"] } ================================================ FILE: examples/bug_234_tls/src/main.rs ================================================ use axum::{Router, response::Html, routing::get}; #[tokio::main] async fn main() { // build our application with a route let app = Router::new().route("/", get(handler)); // run it let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } async fn handler() -> Html<&'static str> { Html("

Hello, World!

") } ================================================ FILE: examples/grpc/Cargo.toml ================================================ [package] name = "examples-grpc" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "otlp", "tracing_subscriber_ext", "logfmt", ] } opentelemetry = { workspace = true } prost = "0.14" tokio = { workspace = true, features = ["full"] } tonic = { workspace = true, features = ["transport", "router"] } tonic-health = "0.14" tonic-prost = "0.14" tonic-reflection = "0.14" tonic-tracing-opentelemetry = { path = "../../tonic-tracing-opentelemetry" } tower = { workspace = true } tracing = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" } [build-dependencies] tonic-prost-build = "0.14" [[bin]] name = "server" path = "src/server.rs" [[bin]] name = "client" path = "src/client.rs" ================================================ FILE: examples/grpc/build.rs ================================================ use std::path::PathBuf; fn main() { // trigger rebuild if "proto" folder change or empty print!("cargo:rerun-if-changed=./proto"); print!("cargo:rerun-if-changed=./src/generated"); //let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let out_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .join("src") .join("generated"); std::fs::create_dir_all(&out_dir).unwrap(); tonic_prost_build::configure() .build_client(true) .build_server(true) .file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin")) .out_dir(out_dir) .compile_protos(&["helloworld.proto"], &["proto"]) .unwrap(); } ================================================ FILE: examples/grpc/proto/helloworld.proto ================================================ syntax = "proto3"; import "google/protobuf/empty.proto"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc SayStatus (StatusRequest) returns (google.protobuf.Empty) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } message StatusRequest { // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc int32 code = 1; string message = 2; } ================================================ FILE: examples/grpc/src/client.rs ================================================ use generated::greeter_client::GreeterClient; use generated::{HelloRequest, StatusRequest}; use tonic::Code; use tonic::transport::Channel; use tonic_tracing_opentelemetry::middleware::client::OtelGrpcLayer; use tower::ServiceBuilder; pub mod generated { //tonic::include_proto!("helloworld"); include!("generated/helloworld.rs"); } #[tokio::main] async fn main() -> Result<(), Box> { // very opinionated init of tracing, look as is source to make your own let _guard = init_tracing_opentelemetry::TracingConfig::production() .init_subscriber() .expect("init subscribers"); // let channel = Channel::from_static("http://[::1]:50051").connect().await?; let channel = Channel::from_static("http://127.0.0.1:50051") .connect() .await?; //Devskim: ignore DS137138 let channel = ServiceBuilder::new().layer(OtelGrpcLayer).service(channel); let mut client = GreeterClient::new(channel); { let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); } { let request = tonic::Request::new(StatusRequest { code: Code::NotFound.into(), message: "not found...".into(), }); let response = client.say_status(request).await; println!("RESPONSE={response:?}"); } { let request = tonic::Request::new(StatusRequest { code: Code::DeadlineExceeded.into(), message: "deadline...".into(), }); let response = client.say_status(request).await; println!("RESPONSE={response:?}"); } Ok(()) } ================================================ FILE: examples/grpc/src/generated/helloworld.rs ================================================ // This file is @generated by prost-build. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HelloRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HelloReply { #[prost(string, tag = "1")] pub message: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct StatusRequest { /// #[prost(int32, tag = "1")] pub code: i32, #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } /// Generated client implementations. pub mod greeter_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; #[derive(Debug, Clone)] pub struct GreeterClient { inner: tonic::client::Grpc, } impl GreeterClient { /// Attempt to create a new client by connecting to a given endpoint. pub async fn connect(dst: D) -> Result where D: TryInto, D::Error: Into, { let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; Ok(Self::new(conn)) } } impl GreeterClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> GreeterClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, , >>::Error: Into + std::marker::Send + std::marker::Sync, { GreeterClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } pub async fn say_hello( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/helloworld.Greeter/SayHello", ); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("helloworld.Greeter", "SayHello")); self.inner.unary(req, path, codec).await } pub async fn say_status( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/helloworld.Greeter/SayStatus", ); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("helloworld.Greeter", "SayStatus")); self.inner.unary(req, path, codec).await } } } /// Generated server implementations. pub mod greeter_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with GreeterServer. #[async_trait] pub trait Greeter: std::marker::Send + std::marker::Sync + 'static { async fn say_hello( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; async fn say_status( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; } #[derive(Debug)] pub struct GreeterServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl GreeterServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor( inner: T, interceptor: F, ) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for GreeterServer where T: Greeter, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/helloworld.Greeter/SayHello" => { #[allow(non_camel_case_types)] struct SayHelloSvc(pub Arc); impl tonic::server::UnaryService for SayHelloSvc { type Response = super::HelloReply; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::say_hello(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = SayHelloSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } "/helloworld.Greeter/SayStatus" => { #[allow(non_camel_case_types)] struct SayStatusSvc(pub Arc); impl tonic::server::UnaryService for SayStatusSvc { type Response = (); type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::say_status(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = SayStatusSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { let mut response = http::Response::new( tonic::body::Body::default(), ); let headers = response.headers_mut(); headers .insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers .insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }) } } } } impl Clone for GreeterServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "helloworld.Greeter"; impl tonic::server::NamedService for GreeterServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: examples/grpc/src/server.rs ================================================ use generated::greeter_server::{Greeter, GreeterServer}; use generated::{HelloReply, HelloRequest, StatusRequest}; use tonic::Code; use tonic::{Request, Response, Status, transport::Server}; use tonic_tracing_opentelemetry::middleware::{filters, server}; pub mod generated { //tonic::include_proto!("helloworld"); include!("generated/helloworld.rs"); pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = //tonic::include_file_descriptor_set!("helloworld_descriptor"); include_bytes!("generated/helloworld_descriptor.bin"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument(skip(self, request))] async fn say_hello( &self, request: Request, ) -> Result, Status> { let trace_id = tracing_opentelemetry_instrumentation_sdk::find_current_trace_id(); tracing::info!( "Got a request from {:?} ({:?})", request.remote_addr(), trace_id ); let reply = generated::HelloReply { message: format!("Hello {}! ({:?})", request.into_inner().name, trace_id), }; Ok(Response::new(reply)) } #[tracing::instrument(skip(self, request))] async fn say_status(&self, request: Request) -> Result, Status> { let trace_id = tracing_opentelemetry_instrumentation_sdk::find_current_trace_id(); let request = request.into_inner(); tracing::info!("ask to return status : {} ({:?})", request.code, trace_id); Err(Status::new(Code::from(request.code), request.message)) } } #[tokio::main] async fn main() -> Result<(), Box> { // very opinionated init of tracing, look as is source to make your own let _guard = init_tracing_opentelemetry::TracingConfig::production() .init_subscriber() .expect("init subscribers"); let addr = "0.0.0.0:50051".parse()?; let greeter = MyGreeter::default(); let (_, health_service) = tonic_health::server::health_reporter(); let reflection_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(generated::FILE_DESCRIPTOR_SET) .build_v1()?; println!("GreeterServer listening on {addr}"); Server::builder() .timeout(std::time::Duration::from_secs(10)) // create trace for every request including health_service .layer(server::OtelGrpcLayer::default().filter(filters::reject_healthcheck)) .add_service(health_service) .add_service(reflection_service) //.add_service(GreeterServer::new(greeter)) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/init-tracing-with/.cargo/config.toml ================================================ [build] rustflags = ["--cfg", "tokio_unstable"] ================================================ FILE: examples/init-tracing-with/Cargo.toml ================================================ [package] name = "init-tracing-with" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "tracing_subscriber_ext", ] } tokio = { version = "1.48.0", features = [ "macros", "rt-multi-thread", "tracing", ] } tokio-blocked = "0.1.0" tracing = { workspace = true } tracing-subscriber = "0.3" ================================================ FILE: examples/init-tracing-with/src/main.rs ================================================ use init_tracing_opentelemetry::TracingConfig; use tokio_blocked::TokioBlockedLayer; use tracing::info; use tracing_subscriber::layer::SubscriberExt; #[tokio::main] async fn main() { let blocked = TokioBlockedLayer::new() .with_warn_busy_single_poll(Some(std::time::Duration::from_micros(150))); let _guard = TracingConfig::default() .with_log_directives("info,tokio::task=trace,tokio::task::waker=warn") .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) .init_subscriber_ext(|subscriber| subscriber.with(blocked)) .unwrap(); info!("will block in 1 secs"); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; tokio::task::spawn(async { // BAD! // This produces a warning log message. info!("blocking!"); std::thread::sleep(std::time::Duration::from_secs(1)); }) .await .unwrap(); // sleep().await; tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } // #[tracing::instrument] // async fn sleep() { // tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; // } ================================================ FILE: examples/load/Cargo.toml ================================================ [package] name = "examples-load" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "otlp", "tracing_subscriber_ext", ] } memory-stats = "1" opentelemetry = { workspace = true } serde_json = "1" tokio = { version = "1", features = ["full"] } tracing = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" } ================================================ FILE: examples/load/README.md ================================================ # examples use to Not an example, but a "load application" used to measure memory usage (+/-) ```sh > bash -c "cargo run --release 2>/dev/null" ... 13s Current memory usage: Some(MemoryStats { physical_mem: 22814720, virtual_mem: 1123610624 }) 13s Current memory usage: Some(MemoryStats { physical_mem: 22835200, virtual_mem: 1123610624 }) 13s Current memory usage: Some(MemoryStats { physical_mem: 22859776, virtual_mem: 1123610624 }) 13s Current memory usage: Some(MemoryStats { physical_mem: 22863872, virtual_mem: 1123610624 }) 14s Current memory usage: Some(MemoryStats { physical_mem: 22867968, virtual_mem: 1123610624 }) 14s Current memory usage: Some(MemoryStats { physical_mem: 22876160, virtual_mem: 1123610624 }) 14s Current memory usage: Some(MemoryStats { physical_mem: 22888448, virtual_mem: 1123610624 }) 15s Current memory usage: Some(MemoryStats { physical_mem: 22892544, virtual_mem: 1123610624 }) 15s Current memory usage: Some(MemoryStats { physical_mem: 22896640, virtual_mem: 1123610624 }) 15s Current memory usage: Some(MemoryStats { physical_mem: 22904832, virtual_mem: 1123610624 }) 16s Current memory usage: Some(MemoryStats { physical_mem: 22921216, virtual_mem: 1123610624 }) 16s Current memory usage: Some(MemoryStats { physical_mem: 22933504, virtual_mem: 1123610624 }) 16s Current memory usage: Some(MemoryStats { physical_mem: 22937600, virtual_mem: 1123610624 }) 16s Current memory usage: Some(MemoryStats { physical_mem: 22941696, virtual_mem: 1123610624 }) 22s Current memory usage: Some(MemoryStats { physical_mem: 22945792, virtual_mem: 1123610624 }) 22s Current memory usage: Some(MemoryStats { physical_mem: 22949888, virtual_mem: 1123610624 }) 28s Current memory usage: Some(MemoryStats { physical_mem: 22970368, virtual_mem: 1123610624 }) 36s Current memory usage: Some(MemoryStats { physical_mem: 22999040, virtual_mem: 1123815424 }) 36s Current memory usage: Some(MemoryStats { physical_mem: 23003136, virtual_mem: 1123815424 }) 36s Current memory usage: Some(MemoryStats { physical_mem: 23007232, virtual_mem: 1123815424 }) 36s Current memory usage: Some(MemoryStats { physical_mem: 23011328, virtual_mem: 1123815424 }) 37s Current memory usage: Some(MemoryStats { physical_mem: 23015424, virtual_mem: 1123815424 }) 38s Current memory usage: Some(MemoryStats { physical_mem: 23207936, virtual_mem: 1123815424 }) 38s Current memory usage: Some(MemoryStats { physical_mem: 22712320, virtual_mem: 1123299328 }) 38s Current memory usage: Some(MemoryStats { physical_mem: 22786048, virtual_mem: 1123459072 }) 38s Current memory usage: Some(MemoryStats { physical_mem: 22872064, virtual_mem: 1123688448 }) 38s Current memory usage: Some(MemoryStats { physical_mem: 22876160, virtual_mem: 1123688448 }) 39s Current memory usage: Some(MemoryStats { physical_mem: 22880256, virtual_mem: 1123688448 }) 40s Current memory usage: Some(MemoryStats { physical_mem: 22888448, virtual_mem: 1123688448 }) 40s Current memory usage: Some(MemoryStats { physical_mem: 22904832, virtual_mem: 1123688448 }) 40s Current memory usage: Some(MemoryStats { physical_mem: 22921216, virtual_mem: 1123688448 }) ... ``` ================================================ FILE: examples/load/src/main.rs ================================================ use std::time::Instant; use memory_stats::memory_stats; use tracing::field::Empty; use tracing_opentelemetry_instrumentation_sdk::otel_trace_span; #[tokio::main] async fn main() -> Result<(), Box> { // very opinionated init of tracing, look as is source to make your own let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; let mut stats = memory_stats(); if stats.is_none() { eprintln!("Couldn't get the current memory usage :("); return Ok(()); } let start = Instant::now(); loop { let prev_stats = stats; stats = memory_stats(); if stats != prev_stats { println!( "{}s Current memory usage: {:?}", start.elapsed().as_secs(), stats ); } for _i in 1..10000 { let _span = otel_trace_span!( "Load", http.request.method = "GET", http.route = Empty, network.protocol.version = "1.1", http.client.address = Empty, http.response.status_code = Empty, otel.kind = "Sever", otel.status_code = Empty, trace_id = Empty, request_id = Empty, exception.message = Empty, //"span.type" = SpanType::Web.to_string(), ) .entered(); //eprintln!("trace_id: {:?}", tracing_opentelemetry_instrumentation_sdk::find_current_trace_id()); } } } ================================================ FILE: examples/logging/Cargo.toml ================================================ [package] name = "examples-logging" publish = false edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true [dependencies] init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ "otlp", "tracing_subscriber_ext", "logs" ] } memory-stats = "1" opentelemetry = { workspace = true } serde_json = "1" tokio = { version = "1", features = ["full"] } tracing = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" } ================================================ FILE: examples/logging/src/main.rs ================================================ #[tracing::instrument] async fn log() { tracing::error!("This is ground control to Major Tom"); tracing::warn!("Houston, we have a problem"); tracing::info!("We have contact"); tracing::debug!("Roger, copy that"); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; } #[tracing::instrument] async fn calc(a: i32, b: i32) { let result = a + b; tracing::info!(result, "calculated result"); } #[tokio::main] async fn main() -> Result<(), Box> { // setting up tracing let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; log().await; calc(1, 2).await; Ok(()) } ================================================ FILE: fake-opentelemetry-collector/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.34.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.34.0...fake-opentelemetry-collector-v0.34.1) - 2026-03-15 ### Fixed - MSRV (bump to 1.88) & api changes in dependencies, reformat ([#325](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/325)) ## [0.34.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.33.1...fake-opentelemetry-collector-v0.34.0) - 2026-01-19 ### Added - *(deps)* update to rust 1.87 & edition 2024 ([#317](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/317)) ## [0.33.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.32.0...fake-opentelemetry-collector-v0.33.0) - 2025-11-22 ### Added - *(metrics)* add support for metrics to the `fake-opentelemetry-collector` ([#302](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/302)) ## [0.32.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.28.0...fake-opentelemetry-collector-v0.32.0) - 2025-06-03 ### Added - *(deps)* update opentelemetry 0.30 & tonic 0.13 (#240) ## [0.28.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.26.1...fake-opentelemetry-collector-v0.28.0) - 2025-03-31 ### Added - *(deps)* update opentelemetry to 0.29 (#227) ## [0.26.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.26.0...fake-opentelemetry-collector-v0.26.1) - 2025-02-26 ### Removed - *(deps)* remove minor constraint when major > 1 ## [0.25.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.24.0...fake-opentelemetry-collector-v0.25.0) - 2024-11-24 ### Fixed - [**breaking**] use `TraceProvider::flush_force()` during test ## [0.21.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.20.0...fake-opentelemetry-collector-v0.21.0) - 2024-09-22 ### Added - *(deps)* upgrade to opentelemetry 0.25 ## [0.17.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/fake-opentelemetry-collector-v0.17.0...fake-opentelemetry-collector-v0.17.1) - 2024-02-24 ### Other - 👷 tune release-plz ================================================ FILE: fake-opentelemetry-collector/Cargo.toml ================================================ [package] name = "fake-opentelemetry-collector" description = "A Fake (basic) opentelemetry collector, useful to test what is collected opentelemetry" readme = "README.md" keywords = ["tracing", "opentelemetry", "faker", "mock"] categories = ["development-tools::testing"] edition.workspace = true version = "0.34.1" repository.workspace = true license.workspace = true [dependencies] futures = "0.3" hex = "0.4" opentelemetry = { workspace = true } opentelemetry-otlp = { workspace = true, features = [ "grpc-tonic", "logs", "trace", "metrics", ] } opentelemetry-proto = { workspace = true, features = [ "gen-tonic", "logs", "trace", ] } # need tokio runtime to run smoke tests. opentelemetry_sdk = { workspace = true, features = [ "trace", "rt-tokio", "testing", ] } serde = { version = "1", features = ["derive"] } tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true, features = ["net"] } tonic = { workspace = true, features = ["codegen", "transport", "router"]} tracing = { workspace = true } [dev-dependencies] assert2 = { workspace = true } insta = { workspace = true } ================================================ FILE: fake-opentelemetry-collector/README.md ================================================ # fake-opentelemetry-collector A Fake (basic) opentelemetry collector, useful to test what is collected by opentelemetry Usage example with [insta](https://crates.io/crates/insta) (snapshot testing) ```rust use std::time::Duration; use fake_opentelemetry_collector::{setup_tracer_provider, FakeCollectorServer}; use opentelemetry::trace::TracerProvider; use opentelemetry::trace::{Span, SpanKind, Tracer}; use tracing::debug; #[tokio::test(flavor = "multi_thread")] async fn demo_fake_tracer_and_collector() { debug!("Start the fake collector"); let mut fake_collector = FakeCollectorServer::start() .await .expect("fake collector setup and started"); debug!("Init the 'application' & tracer provider"); let tracer_provider = setup_tracer_provider(&fake_collector).await; let tracer = tracer_provider.tracer("test"); debug!("Run the 'application' & sending span..."); let mut span = tracer .span_builder("my-test-span") .with_kind(SpanKind::Server) .start(&tracer); span.add_event("my-test-event", vec![]); span.end(); debug!("Shutdown the 'application' & tracer provider and force flush the spans"); let _ = tracer_provider.force_flush(); tracer_provider .shutdown() .expect("no error during shutdown"); drop(tracer_provider); debug!("Collect & check the spans"); let otel_spans = fake_collector .exported_spans(1, Duration::from_secs(20)) .await; //insta::assert_debug_snapshot!(otel_spans); insta::assert_yaml_snapshot!(otel_spans, { "[].start_time_unix_nano" => "[timestamp]", "[].end_time_unix_nano" => "[timestamp]", "[].events[].time_unix_nano" => "[timestamp]", "[].trace_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(trace_id) = value.as_str()); format!("[trace_id:lg{}]", trace_id.len()) }), "[].span_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].links[].trace_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(trace_id) = value.as_str()); format!("[trace_id:lg{}]", trace_id.len()) }), "[].links[].span_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), }); } ``` test example at ================================================ FILE: fake-opentelemetry-collector/src/common.rs ================================================ use std::collections::BTreeMap; pub(crate) fn cnv_attributes( attributes: &[opentelemetry_proto::tonic::common::v1::KeyValue], ) -> BTreeMap { attributes .iter() .map(|kv| (kv.key.to_string(), format!("{:?}", kv.value))) .collect::>() } ================================================ FILE: fake-opentelemetry-collector/src/lib.rs ================================================ mod common; mod logs; mod metrics; mod trace; use logs::*; use metrics::*; use trace::*; pub use logs::ExportedLog; pub use metrics::ExportedMetric; pub use trace::ExportedSpan; use futures::StreamExt; use opentelemetry_otlp::{LogExporter, MetricExporter, SpanExporter, WithExportConfig}; use opentelemetry_proto::tonic::collector::logs::v1::logs_service_server::LogsServiceServer; use opentelemetry_proto::tonic::collector::metrics::v1::metrics_service_server::MetricsServiceServer; use opentelemetry_proto::tonic::collector::trace::v1::trace_service_server::TraceServiceServer; use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use std::net::SocketAddr; use std::time::{Duration, Instant}; use tokio::sync::mpsc; use tokio::sync::mpsc::Receiver; use tokio_stream::wrappers::TcpListenerStream; use tracing::debug; pub struct FakeCollectorServer { address: SocketAddr, req_rx: mpsc::Receiver, log_rx: mpsc::Receiver, metrics_rx: mpsc::Receiver, handle: tokio::task::JoinHandle<()>, } impl FakeCollectorServer { pub async fn start() -> Result> { let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); let listener = tokio::net::TcpListener::bind(addr).await?; let addr = listener.local_addr()?; let stream = TcpListenerStream::new(listener).map(|s| { if let Ok(ref s) = s { debug!("Got new conn at {}", s.peer_addr()?); } s }); let (req_tx, req_rx) = mpsc::channel::(64); let (log_tx, log_rx) = mpsc::channel::(64); let (metrics_tx, metrics_rx) = mpsc::channel::(64); let trace_service = TraceServiceServer::new(FakeTraceService::new(req_tx)); let logs_service = LogsServiceServer::new(FakeLogsService::new(log_tx)); let metrics_service = MetricsServiceServer::new(FakeMetricsService::new(metrics_tx)); let handle = tokio::task::spawn(async move { debug!("start FakeCollectorServer http://{addr}"); //Devskim: ignore DS137138) tonic::transport::Server::builder() .add_service(trace_service) .add_service(logs_service) .add_service(metrics_service) .serve_with_incoming(stream) .await .expect("Server failed"); debug!("stop FakeCollectorServer"); }); Ok(Self { address: addr, req_rx, log_rx, metrics_rx, handle, }) } pub fn address(&self) -> SocketAddr { self.address } pub fn endpoint(&self) -> String { format!("http://{}", self.address()) //Devskim: ignore DS137138) } pub async fn exported_spans( &mut self, at_least: usize, timeout: Duration, ) -> Vec { recv_many(&mut self.req_rx, at_least, timeout).await } pub async fn exported_logs(&mut self, at_least: usize, timeout: Duration) -> Vec { recv_many(&mut self.log_rx, at_least, timeout).await } pub async fn exported_metrics( &mut self, at_least: usize, timeout: Duration, ) -> Vec { recv_many(&mut self.metrics_rx, at_least, timeout).await } pub fn abort(self) { self.handle.abort() } } async fn recv_many(rx: &mut Receiver, at_least: usize, timeout: Duration) -> Vec { let deadline = Instant::now(); let pause = (timeout / 10).min(Duration::from_millis(10)); while rx.len() < at_least && deadline.elapsed() < timeout { tokio::time::sleep(pause).await; } std::iter::from_fn(|| rx.try_recv().ok()).collect::>() } pub async fn setup_tracer_provider( fake_server: &FakeCollectorServer, ) -> opentelemetry_sdk::trace::SdkTracerProvider { // if the environment variable is set (in test or in caller), `with_endpoint` value is ignored // if std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").is_ok() { // panic!( // "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is set, but it should not be, conflict with fake collector" // ); // } unsafe { std::env::remove_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); // unsafe } opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_batch_exporter( SpanExporter::builder() .with_tonic() .with_endpoint(fake_server.endpoint()) .build() .expect("failed to install tracer"), ) .build() } pub async fn setup_logger_provider( fake_server: &FakeCollectorServer, ) -> opentelemetry_sdk::logs::SdkLoggerProvider { opentelemetry_sdk::logs::SdkLoggerProvider::builder() //Install simple so we don't have to wait for batching in tests .with_simple_exporter( LogExporter::builder() .with_tonic() .with_endpoint(fake_server.endpoint()) .build() .expect("failed to install logging"), ) .build() } pub async fn setup_meter_provider( fake_server: &FakeCollectorServer, ) -> opentelemetry_sdk::metrics::SdkMeterProvider { let exporter = MetricExporter::builder() .with_tonic() .with_endpoint(fake_server.endpoint()) .build() .expect("failed to install metrics"); let reader = PeriodicReader::builder(exporter).build(); SdkMeterProvider::builder().with_reader(reader).build() } ================================================ FILE: fake-opentelemetry-collector/src/logs.rs ================================================ use crate::common::cnv_attributes; use opentelemetry_proto::tonic::collector::logs::v1::{ ExportLogsServiceRequest, ExportLogsServiceResponse, logs_service_server::LogsService, }; use serde::Serialize; use std::collections::BTreeMap; use tokio::sync::mpsc; /// This is created to flatten the log record to make it more compatible with insta for testing #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ExportedLog { pub trace_id: String, pub span_id: String, pub observed_time_unix_nano: u64, pub severity_number: i32, pub severity_text: String, pub body: Option, pub attributes: BTreeMap, pub dropped_attributes_count: u32, pub flags: u32, } impl From for ExportedLog { fn from(value: opentelemetry_proto::tonic::logs::v1::LogRecord) -> Self { Self { trace_id: hex::encode(value.trace_id), span_id: hex::encode(value.span_id), observed_time_unix_nano: value.observed_time_unix_nano, severity_number: value.severity_number, severity_text: value.severity_text, body: value.body.map(|value| format!("{value:?}")), attributes: cnv_attributes(&value.attributes), dropped_attributes_count: value.dropped_attributes_count, flags: value.flags, } } } pub(crate) struct FakeLogsService { tx: mpsc::Sender, } impl FakeLogsService { pub fn new(tx: mpsc::Sender) -> Self { Self { tx } } } #[tonic::async_trait] impl LogsService for FakeLogsService { async fn export( &self, request: tonic::Request, ) -> Result, tonic::Status> { let sender = self.tx.clone(); for el in request .into_inner() .resource_logs .into_iter() .flat_map(|rl| rl.scope_logs) .flat_map(|sl| sl.log_records) .map(ExportedLog::from) { sender .send(el) .await .inspect_err(|e| eprintln!("failed to send to channel: {e}")) .map_err(|err| tonic::Status::from_error(Box::new(err)))?; } Ok(tonic::Response::new(ExportLogsServiceResponse { partial_success: None, })) } } ================================================ FILE: fake-opentelemetry-collector/src/metrics.rs ================================================ use crate::common::cnv_attributes; use opentelemetry_proto::tonic::{ collector::metrics::v1::{ ExportMetricsServiceRequest, ExportMetricsServiceResponse, metrics_service_server::MetricsService, }, metrics::v1 as otel_metrics, }; use serde::Serialize; use std::collections::BTreeMap; use tokio::sync::mpsc; pub(crate) struct FakeMetricsService { tx: mpsc::Sender, } impl FakeMetricsService { pub fn new(tx: mpsc::Sender) -> Self { Self { tx } } } #[tonic::async_trait] impl MetricsService for FakeMetricsService { async fn export( &self, request: tonic::Request, ) -> Result, tonic::Status> { let sender = self.tx.clone(); for el in request .into_inner() .resource_metrics .iter() .flat_map(|e| e.scope_metrics.to_vec()) .map(ExportedMetric::from) { sender .send(el) .await .inspect_err(|e| eprintln!("failed to send to channel: {e}")) .map_err(|err| tonic::Status::from_error(Box::new(err)))?; } Ok(tonic::Response::new(ExportMetricsServiceResponse { partial_success: None, })) } } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct ExportedMetric { pub metrics: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Metric { pub name: String, pub description: String, pub unit: String, pub data: Option, } #[derive(Debug, Clone, PartialEq, Serialize)] pub enum MetricsData { Gauge(Gauge), Sum(Sum), Histogram(Histogram), ExponentialHistogram(ExponentialHistogram), Summary(Summary), } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Gauge { pub data_points: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Sum { pub data_points: Vec, pub aggregation_temporality: i32, pub is_monotonic: bool, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Histogram { pub data_points: Vec, pub aggregation_temporality: i32, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct ExponentialHistogram { pub data_points: Vec, pub aggregation_temporality: i32, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Summary { pub data_points: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct NumberDataPoint { pub attributes: BTreeMap, pub start_time_unix_nano: u64, pub time_unix_nano: u64, pub exemplars: Vec, pub flags: u32, pub value: Option, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct HistogramDataPoint { pub attributes: BTreeMap, pub start_time_unix_nano: u64, pub time_unix_nano: u64, pub count: u64, pub sum: Option, pub bucket_counts: Vec, pub explicit_bounds: Vec, pub flags: u32, pub min: Option, pub max: Option, pub exemplars: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct ExponentialHistogramDataPoint { pub attributes: BTreeMap, pub start_time_unix_nano: u64, pub time_unix_nano: u64, pub count: u64, pub sum: Option, pub scale: i32, pub zero_count: u64, pub positive: Option, pub negative: Option, pub flags: u32, pub exemplars: Vec, pub min: Option, pub max: Option, pub zero_threshold: f64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Buckets { pub offset: i32, pub bucket_counts: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct SummaryDataPoint { pub attributes: BTreeMap, pub start_time_unix_nano: u64, pub time_unix_nano: u64, pub count: u64, pub sum: f64, pub quantile_values: Vec, pub flags: u32, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct ValueAtQuantile { pub quantile: f64, pub value: f64, } #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Exemplar { pub filtered_attributes: BTreeMap, pub time_unix_nano: u64, pub span_id: String, pub trace_id: String, pub value: Option, } #[derive(Debug, Clone, PartialEq, Serialize)] pub enum Value { AsDouble(f64), AsInt(i64), } impl From for ExportedMetric { fn from(value: otel_metrics::ScopeMetrics) -> Self { ExportedMetric { metrics: value .metrics .iter() .map(|m| Metric { name: m.name.clone(), description: m.description.clone(), unit: m.unit.clone(), data: m.data.clone().map(Into::into), }) .collect(), } } } impl From for MetricsData { fn from(value: otel_metrics::metric::Data) -> Self { match value { otel_metrics::metric::Data::Gauge(g) => MetricsData::Gauge(g.into()), otel_metrics::metric::Data::Sum(s) => MetricsData::Sum(s.into()), otel_metrics::metric::Data::Histogram(h) => MetricsData::Histogram(h.into()), otel_metrics::metric::Data::ExponentialHistogram(h) => { MetricsData::ExponentialHistogram(h.into()) } otel_metrics::metric::Data::Summary(s) => MetricsData::Summary(s.into()), } } } impl From for Summary { fn from(value: otel_metrics::Summary) -> Self { Self { data_points: value.data_points.iter().map(Into::into).collect(), } } } impl From for ExponentialHistogram { fn from(value: otel_metrics::ExponentialHistogram) -> Self { Self { data_points: value.data_points.iter().map(Into::into).collect(), aggregation_temporality: value.aggregation_temporality, } } } impl From for Histogram { fn from(value: otel_metrics::Histogram) -> Self { Self { data_points: value.data_points.iter().map(Into::into).collect(), aggregation_temporality: value.aggregation_temporality, } } } impl From for Sum { fn from(value: otel_metrics::Sum) -> Self { Self { data_points: value.data_points.iter().map(Into::into).collect(), aggregation_temporality: value.aggregation_temporality, is_monotonic: value.is_monotonic, } } } impl From for Gauge { fn from(value: otel_metrics::Gauge) -> Self { Self { data_points: value.data_points.iter().map(Into::into).collect(), } } } impl From<&otel_metrics::NumberDataPoint> for NumberDataPoint { fn from(value: &otel_metrics::NumberDataPoint) -> Self { Self { attributes: cnv_attributes(&value.attributes), start_time_unix_nano: value.start_time_unix_nano, time_unix_nano: value.time_unix_nano, exemplars: value.exemplars.iter().map(Into::into).collect(), flags: value.flags, value: value.value.map(Into::into), } } } impl From<&otel_metrics::Exemplar> for Exemplar { fn from(value: &otel_metrics::Exemplar) -> Self { Self { filtered_attributes: cnv_attributes(&value.filtered_attributes), time_unix_nano: value.time_unix_nano, span_id: hex::encode(&value.span_id), trace_id: hex::encode(&value.trace_id), value: value.value.map(Into::into), } } } impl From<&otel_metrics::SummaryDataPoint> for SummaryDataPoint { fn from(value: &otel_metrics::SummaryDataPoint) -> Self { Self { attributes: cnv_attributes(&value.attributes), start_time_unix_nano: value.start_time_unix_nano, time_unix_nano: value.time_unix_nano, count: value.count, sum: value.sum, quantile_values: value.quantile_values.iter().map(Into::into).collect(), flags: value.flags, } } } impl From<&otel_metrics::summary_data_point::ValueAtQuantile> for ValueAtQuantile { fn from(value: &otel_metrics::summary_data_point::ValueAtQuantile) -> Self { Self { quantile: value.quantile, value: value.value, } } } impl From<&otel_metrics::HistogramDataPoint> for HistogramDataPoint { fn from(value: &otel_metrics::HistogramDataPoint) -> Self { Self { attributes: cnv_attributes(&value.attributes), start_time_unix_nano: value.start_time_unix_nano, time_unix_nano: value.time_unix_nano, count: value.count, sum: value.sum, bucket_counts: value.bucket_counts.to_vec(), flags: value.flags, explicit_bounds: value.explicit_bounds.to_vec(), max: value.max, min: value.min, exemplars: value.exemplars.iter().map(Into::into).collect(), } } } impl From<&otel_metrics::ExponentialHistogramDataPoint> for ExponentialHistogramDataPoint { fn from(value: &otel_metrics::ExponentialHistogramDataPoint) -> Self { Self { attributes: cnv_attributes(&value.attributes), start_time_unix_nano: value.start_time_unix_nano, time_unix_nano: value.time_unix_nano, count: value.count, sum: value.sum, scale: value.scale, zero_count: value.zero_count, positive: value.positive.as_ref().map(Into::into), negative: value.negative.as_ref().map(Into::into), flags: value.flags, exemplars: value.exemplars.iter().map(Into::into).collect(), max: value.max, min: value.min, zero_threshold: value.zero_threshold, } } } impl From<&otel_metrics::exponential_histogram_data_point::Buckets> for Buckets { fn from(value: &otel_metrics::exponential_histogram_data_point::Buckets) -> Self { Self { offset: value.offset, bucket_counts: value.bucket_counts.to_vec(), } } } impl From for Value { fn from(value: otel_metrics::exemplar::Value) -> Self { match value { otel_metrics::exemplar::Value::AsDouble(n) => Value::AsDouble(n), otel_metrics::exemplar::Value::AsInt(n) => Value::AsInt(n), } } } impl From for Value { fn from(value: otel_metrics::number_data_point::Value) -> Self { match value { otel_metrics::number_data_point::Value::AsDouble(n) => Value::AsDouble(n), otel_metrics::number_data_point::Value::AsInt(n) => Value::AsInt(n), } } } ================================================ FILE: fake-opentelemetry-collector/src/trace.rs ================================================ //! based on https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/tests/smoke.rs use crate::common::cnv_attributes; use opentelemetry_proto::tonic::collector::trace::v1::{ ExportTraceServiceRequest, ExportTraceServiceResponse, trace_service_server::TraceService, }; use serde::Serialize; use std::collections::BTreeMap; use tokio::sync::mpsc; use tracing::debug; /// opentelemetry_proto::tonic::trace::v1::Span is no compatible with serde::Serialize /// and to be able to test with insta,... it's needed (Debug is not enough to be able to filter unstable value,...) #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ExportedSpan { pub trace_id: String, pub span_id: String, pub trace_state: String, pub parent_span_id: String, pub name: String, pub kind: String, //SpanKind, pub start_time_unix_nano: u64, pub end_time_unix_nano: u64, pub attributes: BTreeMap, pub dropped_attributes_count: u32, pub events: Vec, pub dropped_events_count: u32, pub links: Vec, pub dropped_links_count: u32, pub status: Option, } impl From for ExportedSpan { fn from(value: opentelemetry_proto::tonic::trace::v1::Span) -> Self { Self { trace_id: hex::encode(&value.trace_id), span_id: hex::encode(&value.span_id), trace_state: value.trace_state.clone(), parent_span_id: hex::encode(&value.parent_span_id), name: value.name.clone(), kind: value.kind().as_str_name().to_owned(), start_time_unix_nano: value.start_time_unix_nano, end_time_unix_nano: value.end_time_unix_nano, attributes: cnv_attributes(&value.attributes), dropped_attributes_count: value.dropped_attributes_count, events: value.events.iter().map(Event::from).collect(), dropped_events_count: value.dropped_events_count, links: value.links.iter().map(Link::from).collect(), dropped_links_count: value.dropped_links_count, status: value.status.map(Status::from), } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize)] pub struct Status { pub message: String, pub code: String, } impl From for Status { fn from(value: opentelemetry_proto::tonic::trace::v1::Status) -> Self { Self { message: value.message.clone(), code: value.code().as_str_name().to_string(), } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Link { pub trace_id: String, pub span_id: String, pub trace_state: String, pub attributes: BTreeMap, pub dropped_attributes_count: u32, } impl From<&opentelemetry_proto::tonic::trace::v1::span::Link> for Link { fn from(value: &opentelemetry_proto::tonic::trace::v1::span::Link) -> Self { Self { trace_id: hex::encode(&value.trace_id), span_id: hex::encode(&value.span_id), trace_state: value.trace_state.clone(), attributes: cnv_attributes(&value.attributes), dropped_attributes_count: value.dropped_attributes_count, } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Event { pub time_unix_nano: u64, pub name: String, pub attributes: BTreeMap, pub dropped_attributes_count: u32, } impl From<&opentelemetry_proto::tonic::trace::v1::span::Event> for Event { fn from(value: &opentelemetry_proto::tonic::trace::v1::span::Event) -> Self { Self { time_unix_nano: value.time_unix_nano, name: value.name.clone(), attributes: cnv_attributes(&value.attributes), dropped_attributes_count: value.dropped_attributes_count, } } } pub(crate) struct FakeTraceService { tx: mpsc::Sender, } impl FakeTraceService { pub fn new(tx: mpsc::Sender) -> Self { Self { tx } } } #[tonic::async_trait] impl TraceService for FakeTraceService { async fn export( &self, request: tonic::Request, ) -> Result, tonic::Status> { debug!("Sending request into channel..."); let sender = self.tx.clone(); for es in request .into_inner() .resource_spans .into_iter() .flat_map(|rs| rs.scope_spans) .flat_map(|ss| ss.spans) .map(ExportedSpan::from) { sender .send(es) .await .inspect_err(|e| eprintln!("failed to send to channel: {e}")) .map_err(|err| tonic::Status::from_error(Box::new(err)))?; } Ok(tonic::Response::new(ExportTraceServiceResponse { partial_success: None, })) } } ================================================ FILE: fake-opentelemetry-collector/tests/demo_log.rs ================================================ use std::time::Duration; use fake_opentelemetry_collector::{FakeCollectorServer, setup_logger_provider}; use opentelemetry::logs::{LogRecord, Logger, LoggerProvider, Severity}; use tracing::debug; #[tokio::test(flavor = "multi_thread")] async fn demo_fake_logger_and_collector() { debug!("Start the fake collector"); let mut fake_collector = FakeCollectorServer::start() .await .expect("fake collector setup and started"); debug!("Init the 'application' & logger provider"); let logger_provider = setup_logger_provider(&fake_collector).await; let logger = logger_provider.logger("test"); debug!("Run the 'application' & send log ..."); let mut record = logger.create_log_record(); record.set_body("This is information".into()); record.set_severity_number(Severity::Info); record.set_severity_text("info"); logger.emit(record); debug!("Shutdown the 'application' & logger provider"); let _ = logger_provider.force_flush(); logger_provider .shutdown() .expect("no error during shutdown"); drop(logger_provider); debug!("Collect & check the logs"); let otel_logs = fake_collector .exported_logs(1, Duration::from_millis(500)) .await; insta::assert_yaml_snapshot!(otel_logs, { "[].trace_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(trace_id) = value.as_str()); format!("[trace_id:lg{}]", trace_id.len()) }), "[].span_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].observed_time_unix_nano" => "[timestamp]", "[].severity_number" => 9, "[].severity_text" => "info", "[].body" => "AnyValue { value: Some(StringValue(\"This is information\")) }", }); } ================================================ FILE: fake-opentelemetry-collector/tests/demo_metrics.rs ================================================ use fake_opentelemetry_collector::{FakeCollectorServer, setup_meter_provider}; use opentelemetry::{KeyValue, global}; use std::time::Duration; use tracing::debug; #[tokio::test(flavor = "multi_thread")] async fn demo_fake_meter_and_collector() { debug!("Start the fake collector"); let mut fake_collector = FakeCollectorServer::start() .await .expect("fake collector setup and started"); debug!("Init the 'application' & meter provider"); let meter_provider = setup_meter_provider(&fake_collector).await; global::set_meter_provider(meter_provider.clone()); debug!("Run the 'application' & send metrics ..."); let meter = global::meter("test"); let attributes = &[KeyValue::new("foo", "bar")]; let gauge = meter .f64_gauge("test_gauge") .with_description("A test gauge") .with_unit("km/s") .build(); gauge.record(123.456, attributes); let up_down_counter = meter .i64_up_down_counter("test_updown_counter") .with_description("A test up-down-counter") .with_unit("m/s^2") .build(); up_down_counter.add(-50, attributes); let counter = meter .u64_counter("test_counter") .with_description("A test counter") .with_unit("Jigawatts") .build(); counter.add(25, attributes); let histogram = meter .u64_histogram("test_histogram") .with_description("A test histogram") .with_unit("ft/in^2") .build(); histogram.record(10, attributes); histogram.record(13, attributes); debug!("Shutdown the 'application' & meter provider"); meter_provider.shutdown().expect("no error during shutdown"); drop(meter_provider); debug!("Collect & check the metrics"); let otel_metrics = fake_collector .exported_metrics(1, Duration::from_millis(500)) .await; insta::assert_yaml_snapshot!(otel_metrics, { // Validate gauge metric "[0].metrics[0].name" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(name) = value.as_str()); assert_eq!(name, "test_gauge"); name.to_string() }), "[0].metrics[0].description" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(desc) = value.as_str()); assert_eq!(desc, "A test gauge"); desc.to_string() }), "[0].metrics[0].unit" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(unit) = value.as_str()); assert_eq!(unit, "km/s"); unit.to_string() }), "[0].metrics[0].data.Gauge.data_points[0].value.AsDouble" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(val) = value.as_f64()); assert!((val - 123.456).abs() < 0.001); format!("{val}") }), // Validate up-down counter "[0].metrics[1].name" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(name) = value.as_str()); assert_eq!(name, "test_updown_counter"); name.to_string() }), "[0].metrics[1].description" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(desc) = value.as_str()); assert_eq!(desc, "A test up-down-counter"); desc.to_string() }), "[0].metrics[1].unit" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(unit) = value.as_str()); assert_eq!(unit, "m/s^2"); unit.to_string() }), "[0].metrics[1].data.Sum.data_points[0].value.AsInt" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(val) = value.as_i64()); assert_eq!(val, -50); format!("{val}") }), "[0].metrics[1].data.Sum.is_monotonic" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(monotonic) = value.as_bool()); assert!(!monotonic); format!("{monotonic}") }), "[0].metrics[1].data.Sum.aggregation_temporality" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(temporality) = value.as_u64()); assert_eq!(temporality, 2); // Cumulative format!("{temporality}") }), // Validate counter "[0].metrics[2].name" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(name) = value.as_str()); assert_eq!(name, "test_counter"); name.to_string() }), "[0].metrics[2].description" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(desc) = value.as_str()); assert_eq!(desc, "A test counter"); desc.to_string() }), "[0].metrics[2].unit" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(unit) = value.as_str()); assert_eq!(unit, "Jigawatts"); unit.to_string() }), "[0].metrics[2].data.Sum.data_points[0].value.AsInt" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(val) = value.as_i64()); assert_eq!(val, 25); format!("{val}") }), "[0].metrics[2].data.Sum.is_monotonic" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(monotonic) = value.as_bool()); assert!(monotonic); format!("{monotonic}") }), "[0].metrics[2].data.Sum.aggregation_temporality" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(temporality) = value.as_u64()); assert_eq!(temporality, 2); // Cumulative format!("{temporality}") }), // Validate histogram "[0].metrics[3].name" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(name) = value.as_str()); assert_eq!(name, "test_histogram"); name.to_string() }), "[0].metrics[3].description" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(desc) = value.as_str()); assert_eq!(desc, "A test histogram"); desc.to_string() }), "[0].metrics[3].unit" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(unit) = value.as_str()); assert_eq!(unit, "ft/in^2"); unit.to_string() }), "[0].metrics[3].data.Histogram.data_points[0].count" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(count) = value.as_u64()); assert_eq!(count, 2); format!("{count}") }), "[0].metrics[3].data.Histogram.data_points[0].sum" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(sum) = value.as_u64()); assert_eq!(sum, 23); // 10 + 13 format!("{sum}") }), "[0].metrics[3].data.Histogram.data_points[0].min" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(min) = value.as_u64()); assert_eq!(min, 10); format!("{min}") }), "[0].metrics[3].data.Histogram.data_points[0].max" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(max) = value.as_u64()); assert_eq!(max, 13); format!("{max}") }), "[0].metrics[3].data.Histogram.aggregation_temporality" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(temporality) = value.as_u64()); assert_eq!(temporality, 2); // Cumulative format!("{temporality}") }), // Validate attributes for all metrics "[].metrics[].data.**.attributes.foo" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(attr_value) = value.as_str()); assert!(attr_value.contains("bar")); "\"Some(AnyValue { value: Some(StringValue(\\\"bar\\\")) })\"" }), // Redact timestamps "[].metrics[].data.**.start_time_unix_nano" => "[timestamp]", "[].metrics[].data.**.time_unix_nano" => "[timestamp]", "[].metrics[].data.**.exemplars[].time_unix_nano" => "[timestamp]", }); } ================================================ FILE: fake-opentelemetry-collector/tests/demo_trace.rs ================================================ use std::time::Duration; use fake_opentelemetry_collector::{FakeCollectorServer, setup_tracer_provider}; use opentelemetry::trace::TracerProvider; use opentelemetry::trace::{Span, SpanKind, Tracer}; use tracing::debug; #[tokio::test(flavor = "multi_thread")] async fn demo_fake_tracer_and_collector() { debug!("Start the fake collector"); let mut fake_collector = FakeCollectorServer::start() .await .expect("fake collector setup and started"); debug!("Init the 'application' & tracer provider"); let tracer_provider = setup_tracer_provider(&fake_collector).await; let tracer = tracer_provider.tracer("test"); debug!("Run the 'application' & sending span..."); let mut span = tracer .span_builder("my-test-span") .with_kind(SpanKind::Server) .start(&tracer); span.add_event("my-test-event", vec![]); span.end(); debug!("Shutdown the 'application' & tracer provider and force flush the spans"); let _ = tracer_provider.force_flush(); tracer_provider .shutdown() .expect("no error during shutdown"); drop(tracer_provider); debug!("Collect & check the spans"); let otel_spans = fake_collector .exported_spans(1, Duration::from_secs(20)) .await; //insta::assert_debug_snapshot!(otel_spans); insta::assert_yaml_snapshot!(otel_spans, { "[].start_time_unix_nano" => "[timestamp]", "[].end_time_unix_nano" => "[timestamp]", "[].events[].time_unix_nano" => "[timestamp]", "[].trace_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(trace_id) = value.as_str()); format!("[trace_id:lg{}]", trace_id.len()) }), "[].span_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].links[].trace_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(trace_id) = value.as_str()); format!("[trace_id:lg{}]", trace_id.len()) }), "[].links[].span_id" => insta::dynamic_redaction(|value, _path| { assert2::assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), }); } ================================================ FILE: fake-opentelemetry-collector/tests/snapshots/demo_log__demo_fake_logger_and_collector.snap ================================================ --- source: fake-opentelemetry-collector/tests/demo_log.rs expression: otel_logs snapshot_kind: text --- - trace_id: "[trace_id:lg0]" span_id: "[span_id:lg0]" observed_time_unix_nano: "[timestamp]" severity_number: 9 severity_text: info body: "AnyValue { value: Some(StringValue(\"This is information\")) }" attributes: {} dropped_attributes_count: 0 flags: 0 ================================================ FILE: fake-opentelemetry-collector/tests/snapshots/demo_metrics__demo_fake_meter_and_collector.snap ================================================ --- source: fake-opentelemetry-collector/tests/demo_metrics.rs expression: otel_metrics --- - metrics: - name: test_gauge description: A test gauge unit: km/s data: Gauge: data_points: - attributes: foo: "\"Some(AnyValue { value: Some(StringValue(\\\"bar\\\")) })\"" start_time_unix_nano: "[timestamp]" time_unix_nano: "[timestamp]" exemplars: [] flags: 0 value: AsDouble: 123.456 - name: test_updown_counter description: A test up-down-counter unit: m/s^2 data: Sum: data_points: - attributes: foo: "\"Some(AnyValue { value: Some(StringValue(\\\"bar\\\")) })\"" start_time_unix_nano: "[timestamp]" time_unix_nano: "[timestamp]" exemplars: [] flags: 0 value: AsInt: -50 aggregation_temporality: 2 is_monotonic: false - name: test_counter description: A test counter unit: Jigawatts data: Sum: data_points: - attributes: foo: "\"Some(AnyValue { value: Some(StringValue(\\\"bar\\\")) })\"" start_time_unix_nano: "[timestamp]" time_unix_nano: "[timestamp]" exemplars: [] flags: 0 value: AsInt: 25 aggregation_temporality: 2 is_monotonic: true - name: test_histogram description: A test histogram unit: ft/in^2 data: Histogram: data_points: - attributes: foo: "\"Some(AnyValue { value: Some(StringValue(\\\"bar\\\")) })\"" start_time_unix_nano: "[timestamp]" time_unix_nano: "[timestamp]" count: 2 sum: 23 bucket_counts: - 0 - 0 - 1 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 explicit_bounds: - 0 - 5 - 10 - 25 - 50 - 75 - 100 - 250 - 500 - 750 - 1000 - 2500 - 5000 - 7500 - 10000 flags: 0 min: 10 max: 13 exemplars: [] aggregation_temporality: 2 ================================================ FILE: fake-opentelemetry-collector/tests/snapshots/demo_trace__demo_fake_tracer_and_collector.snap ================================================ --- source: fake-opentelemetry-collector/tests/demo_trace.rs expression: otel_spans snapshot_kind: text --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "" name: my-test-span kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: {} dropped_attributes_count: 0 events: - time_unix_nano: "[timestamp]" name: my-test-event attributes: {} dropped_attributes_count: 0 dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: init-tracing-opentelemetry/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.37.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.36.1...init-tracing-opentelemetry-v0.37.0) - 2026-04-27 ### Fixed - *(init)* metrics enabled if feature flages & explicit enabling - avoid to log sensitive OTEL value, and warn if issue on logfmt - *(docs)* build of sample & add screenshot for log ### Added - accept owned string for service'name and version - add OTLP log exporter ([#333](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/333)) ## [0.37.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.36.1...init-tracing-opentelemetry-v0.37.0) - 2026-04-27 ### Fixed - *(init)* metrics enabled if feature flages & explicit enabling - avoid to log sensitive OTEL value, and warn if issue on logfmt - *(docs)* build of sample & add screenshot for log ### Added - accept owned string for service'name and version - add OTLP log exporter ([#333](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/333)) ## [0.36.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.36.0...init-tracing-opentelemetry-v0.36.1) - 2026-03-15 ### Fixed - MSRV (bump to 1.88) & api changes in dependencies, reformat ([#325](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/325)) ## [0.36.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.36.0...init-tracing-opentelemetry-v0.36.1) - 2026-03-15 ### Fixed - MSRV (bump to 1.88) & api changes in dependencies, reformat ([#325](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/325)) ## [0.36.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/releases/tag/init-tracing-opentelemetry-v0.36.0) - 2026-01-19 ### Added - *(deps)* update to rust 1.87 & edition 2024 ([#317](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/317)) ## [0.36.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.35.0...init-tracing-opentelemetry-v0.36.0) - 2026-01-19 ### Added - *(deps)* update to rust 1.87 & edition 2024 ([#317](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/317)) ## [0.35.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.34.0...init-tracing-opentelemetry-v0.35.0) - 2026-01-12 ### Added - allow to define otel tracer's name via `TracerConfig` ([#313](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/313)) ## [0.34.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.33.0...init-tracing-opentelemetry-v0.34.0) - 2025-11-13 ### Fixed - *(init-tracing-opentelemetry)* apply custom resource configuration to OpenTelemetry layers ([#297](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/297)) - Fix timers, booleans add support for `Layer::without_time` ([#295](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/295)) ### Added - Add support for `Layer::with_thread_ids` - Add support for `Layer::with_file` - add support for `tracing_subscriber::fmt::format::Full` ([#291](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/291)) - add in features to docs.rs rendered content. ([#287](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/287)) ## [0.33.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.32.1...init-tracing-opentelemetry-v0.33.0) - 2025-11-02 ### Added - allow to customize the tracing configuration (registry) with additional layers ([#281](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/281)) ## [0.31.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.30.1...init-tracing-opentelemetry-v0.31.0) - 2025-09-27 ### Added - [**breaking**] Guard struct allow future evolution, init_subscriber can be used for non global (like test,...) - a more configurable tracing configuration with `TracingConfig` ## [0.30.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.29.0...init-tracing-opentelemetry-v0.30.0) - 2025-07-18 ### Added - add support for Opentelemetry Metrics (#249) ## [0.28.2](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.28.1...init-tracing-opentelemetry-v0.28.2) - 2025-06-03 ### Fixed - *(deps)* missing `tonic` dependency on `tls` ### Removed - remove deprecated sample from README ## [0.28.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.27.1...init-tracing-opentelemetry-v0.28.0) - 2025-03-31 ### Added - *(deps)* update opentelemetry to 0.29 (#227) ## [0.27.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.27.0...init-tracing-opentelemetry-v0.27.1) - 2025-02-26 ### Fixed - reqwest must use blocking client since opentelemetry 0.28 (#220) ### Removed - *(deps)* remove minor constraint when major > 1 ## [0.27.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.26.0...init-tracing-opentelemetry-v0.27.0) - 2025-02-24 ### Fixed - drop on the TracingGuard also shutdown the wrapped TracerProvider ### Added - allow to provide log's "directives" via `init_subscribers_and_loglevel` ## [0.25.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.24.1...init-tracing-opentelemetry-v0.25.0) - 2024-12-10 ### Fixed - inference of `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` for protocol `http/protobuf` ## [0.24.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.24.0...init-tracing-opentelemetry-v0.24.1) - 2024-11-24 ### Fixed - Use guard pattern to allow consumers to ensure final trace is sent ([#185](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/185)) ## [0.24.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.21.0...init-tracing-opentelemetry-v0.24.0) - 2024-09-23 ### Added - [**breaking**] remove trace_id and span_id from logfmt (to avoid link with old version) ## [0.21.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.20.0...init-tracing-opentelemetry-v0.21.0) - 2024-09-22 ### Added - *(deps)* upgrade to opentelemetry 0.25 - add a troubleshot section - [**breaking**] disable resourcedetector (os,...) until update for new version of opentelemetry - [**breaking**] disable support of xray (until update for new version of opentelemetry) ## [0.20.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.19.0...init-tracing-opentelemetry-v0.20.0) - 2024-08-31 ### Fixed - 🐛 fix build of contributions (upgrade of opentelemetry, fake collector for logs,...) ### Changed - ⬆️ upgrade to rstest 0.22 - ⬆️ upgrade to opentelemetry 0.24 (and related dependencies) ([#151](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/151)) ## [0.17.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/init-tracing-opentelemetry-v0.17.0...init-tracing-opentelemetry-v0.17.1) - 2024-02-24 ### Other - 👷 tune release-plz ================================================ FILE: init-tracing-opentelemetry/Cargo.toml ================================================ [package] name = "init-tracing-opentelemetry" description = "A set of helpers to initialize (and more) tracing + opentelemetry (compose your own or use opinionated preset)" readme = "README.md" keywords = ["tracing", "opentelemetry"] categories = ["development-tools::debugging", "development-tools::profiling"] homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/init-tracing-opentelemetry" edition.workspace = true version = "0.37.0" repository.workspace = true license.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docs_rs"] [dependencies] opentelemetry = { workspace = true } opentelemetry-appender-tracing = { version = "0.31", default-features = false, optional = true } opentelemetry-aws = { workspace = true, optional = true, features = ["trace"] } opentelemetry-jaeger-propagator = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true, features = [ "grpc-tonic", "trace", ] } # opentelemetry-resource-detectors = { workspace = true } //FIXME enable when available for opentelemetry >= 0.25 opentelemetry-stdout = { workspace = true, features = [ "trace", ], optional = true } opentelemetry-semantic-conventions = { workspace = true, optional = true } opentelemetry-zipkin = { workspace = true, features = [], optional = true } opentelemetry_sdk = { workspace = true, features = ["trace"] } thiserror = "2" tonic = { workspace = true, optional = true } tracing = { workspace = true } tracing-logfmt = { version = "0.3", optional = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { version = "0.3", default-features = false, features = [ "ansi", "env-filter", "fmt", "json", ], optional = true } [dev-dependencies] assert2 = { workspace = true } rstest = { workspace = true } # need tokio runtime to run smoke tests and rust doc tracing-opentelemetry-instrumentation-sdk = { path = "../tracing-opentelemetry-instrumentation-sdk" } opentelemetry_sdk = { workspace = true, features = [ "trace", "rt-tokio", "testing", ] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } tokio-blocked = "0.1" tokio-stream = { version = "0.1" } tracing-subscriber = { version = "0.3", default-features = false, features = [ "env-filter", "fmt", "json", ] } [features] jaeger = ["dep:opentelemetry-jaeger-propagator"] otlp = [ "opentelemetry-otlp/http-proto", "opentelemetry-otlp/reqwest-blocking-client", "opentelemetry-otlp/reqwest-rustls", "tracer", ] stdout = ["dep:opentelemetry-stdout", "tracer"] tracer = ["dep:opentelemetry-semantic-conventions"] xray = ["dep:opentelemetry-aws"] zipkin = ["dep:opentelemetry-zipkin"] tracing_subscriber_ext = ["dep:tracing-subscriber", "otlp"] tls = ["opentelemetry-otlp/tls", "tonic"] tls-roots = ["opentelemetry-otlp/tls-roots"] tls-webpki-roots = ["opentelemetry-otlp/tls-webpki-roots"] logfmt = ["dep:tracing-logfmt"] logs = [ "dep:opentelemetry-appender-tracing", "opentelemetry-otlp/logs", "opentelemetry_sdk/logs", "opentelemetry/logs", ] metrics = [ "opentelemetry-otlp/metrics", "tracing-opentelemetry/metrics", "opentelemetry-stdout/metrics", ] [lints] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docs_rs)'] } ================================================ FILE: init-tracing-opentelemetry/README.md ================================================ # init-tracing-opentelemetry [![crates license](https://img.shields.io/crates/l/init-tracing-opentelemetry.svg)](http://creativecommons.org/publicdomain/zero/1.0/) [![crate version](https://img.shields.io/crates/v/init-tracing-opentelemetry.svg)](https://crates.io/crates/init-tracing-opentelemetry) A set of helpers to initialize (and more) tracing + opentelemetry (compose your own or use opinionated preset) ```rust #[tokio::main] async fn main() -> Result<(), Box> { // Simple preset let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; //... Ok(()) } ``` ```rust #[tokio::main] async fn main() -> Result<(), Box> { // custom configuration let _guard = init_tracing_opentelemetry::TracingConfig::default() .with_json_format() .with_stderr() .with_log_directives("debug") .init_subscriber()?; //... Ok(()) } ``` The `init_subscriber()` function returns an `OtelGuard` instance. Following the guard pattern, this struct provides no functions but, when dropped, ensures that any pending traces/metrics are sent before it exits. The syntax `let _guard` is suggested to ensure that Rust does not drop the struct until the application exits. ## Configuration Options ### Presets - `TracingConfig::development()` - Pretty format, stderr, with debug info - `TracingConfig::production()` - JSON format, stdout, minimal metadata - `TracingConfig::debug()` - Full verbosity with all span events - `TracingConfig::minimal()` - Compact format, no OpenTelemetry - `TracingConfig::testing()` - Minimal output for tests ### Custom Configuration ```rust,no_run use init_tracing_opentelemetry::TracingConfig; TracingConfig::default() .with_pretty_format() // or .with_json_format(), .with_compact_format() .with_stderr() // or .with_stdout(), .with_file(path) .with_log_directives("debug") // Custom log levels .with_line_numbers(true) // Include line numbers .with_thread_names(true) // Include thread names .with_otel(true) // Enable OpenTelemetry .init_subscriber() .expect("valid tracing configuration"); ``` ### Add custom layer, modify subscriber Use `init_subscriber_ext(|subscriber| {...} )` to transform the subscriber (registry), before application of the configuration. ```rust use init_tracing_opentelemetry::TracingConfig; use tokio_blocked::TokioBlockedLayer; use tracing::info; use tracing_subscriber::layer::SubscriberExt; #[tokio::main] async fn main() { let blocked = TokioBlockedLayer::new() .with_warn_busy_single_poll(Some(std::time::Duration::from_micros(150))); let _guard = TracingConfig::default() .with_log_directives("info,tokio::task=trace,tokio::task::waker=warn") .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) .init_subscriber_ext(|subscriber| subscriber.with(blocked)) .unwrap(); tokio::task::spawn(async { // BAD! // This produces a warning log message. info!("blocking!"); std::thread::sleep(std::time::Duration::from_secs(1)); }) .await .unwrap(); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } ``` ### Legacy API (deprecated) For backward compatibility, the old API is still available: ```txt pub fn build_loglevel_filter_layer() -> tracing_subscriber::filter::EnvFilter { // filter what is output on log (fmt) // std::env::set_var("RUST_LOG", "warn,axum_tracing_opentelemetry=info,otel=debug"); std::env::set_var( "RUST_LOG", format!( // `otel::tracing` should be a level trace to emit opentelemetry trace & span // `otel::setup` set to debug to log detected resources, configuration read and infered "{},otel::tracing=trace,otel=debug", std::env::var("RUST_LOG") .or_else(|_| std::env::var("OTEL_LOG_LEVEL")) .unwrap_or_else(|_| "info".to_string()) ), ); EnvFilter::from_default_env() } pub fn build_otel_layer() -> Result, BoxError> where S: Subscriber + for<'a> LookupSpan<'a>, { use crate::{ init_propagator, //stdio, otlp, resource::DetectResource, }; let otel_rsrc = DetectResource::default() //.with_fallback_service_name(env!("CARGO_PKG_NAME")) //.with_fallback_service_version(env!("CARGO_PKG_VERSION")) .build(); let otel_tracer = otlp::init_tracer(otel_rsrc, otlp::identity)?; // to not send trace somewhere, but continue to create and propagate,... // then send them to `axum_tracing_opentelemetry::stdio::WriteNoWhere::default()` // or to `std::io::stdout()` to print // // let otel_tracer = // stdio::init_tracer(otel_rsrc, stdio::identity, stdio::WriteNoWhere::default())?; init_propagator()?; Ok(tracing_opentelemetry::layer().with_tracer(otel_tracer)) } ``` To retrieve the current `trace_id` (eg to add it into error message (as header or attributes)) ```rust # use tracing_opentelemetry_instrumentation_sdk; let trace_id = tracing_opentelemetry_instrumentation_sdk::find_current_trace_id(); //json!({ "error" : "xxxxxx", "trace_id": trace_id}) ``` ## Configuration based on the environment variables To ease setup and compliance with [OpenTelemetry SDK configuration](https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/), the configuration can be done with the following environment variables (see sample `init_tracing()` above): - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` fallback to `OTEL_EXPORTER_OTLP_ENDPOINT` for the url of the exporter / collector - `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` fallback to `OTEL_EXPORTER_OTLP_PROTOCOL`, fallback to auto-detection based on ENDPOINT port - `OTEL_SERVICE_NAME` for the name of the service - `OTEL_PROPAGATORS` for the configuration of the propagators - `OTEL_TRACES_SAMPLER` & `OTEL_TRACES_SAMPLER_ARG` for configuration of the sampler Few other environment variables can also be used to configure OTLP exporter (eg to configure headers, authentication,, etc...): - [`OTEL_EXPORTER_OTLP_HEADERS`](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers) - [`OTEL_EXPORTER_OTLP_TRACES_HEADERS`](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_traces_headers) ```sh # For GRPC: export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4317" export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="grpc" export OTEL_TRACES_SAMPLER="always_on" # For HTTP: export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://127.0.0.1:4318/v1/traces" export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf" export OTEL_TRACES_SAMPLER="always_on" ``` In the context of **kubernetes**, some of the above environment variables can be injected by the Opentelemetry operator (via `inject-sdk`): ```yaml apiVersion: apps/v1 kind: Deployment spec: template: metadata: annotations: # to inject environment variables only by opentelemetry-operator instrumentation.opentelemetry.io/inject-sdk: "opentelemetry-operator/instrumentation" instrumentation.opentelemetry.io/container-names: "app" containers: - name: app ``` Or if you don't setup `inject-sdk`, you can manually set the environment variable eg ```yaml apiVersion: apps/v1 kind: Deployment spec: template: metadata: containers: - name: app env: - name: OTEL_SERVICE_NAME value: "app" - name: OTEL_EXPORTER_OTLP_PROTOCOL value: "grpc" # for otel collector in `deployment` mode, use the name of the service # - name: OTEL_EXPORTER_OTLP_ENDPOINT # value: "http://opentelemetry-collector.opentelemetry-collector:4317" # for otel collector in sidecar mode (imply to deploy a sidecar CR per namespace) - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "http://localhost:4317" # for `daemonset` mode: need to use the local daemonset (value interpolated by k8s: `$(...)`) # - name: OTEL_EXPORTER_OTLP_ENDPOINT # value: "http://$(HOST_IP):4317" # - name: HOST_IP # valueFrom: # fieldRef: # fieldPath: status.hostIP ``` ## Troubleshot why no trace? - check you only have a single version of opentelemtry (could be part of your CI/build), use `cargo-deny` or `cargo tree` ```sh # Check only one version of opentelemetry should be used # else issue with setup of global (static variable) # check_single_version_opentelemtry: cargo tree -i opentelemetry ``` - check the code of your exporter and the integration with `tracing` (as subscriber's layer) - check the environment variables of opentelemetry `OTEL_EXPORTER...` and `OTEL_TRACES_SAMPLER` (values are logged on target `otel::setup` ) - check that log target `otel::tracing` enable log level `trace` (or `info` if you use `tracing_level_info` feature) to generate span to send to opentelemetry collector. ## Metrics To configure opentelemetry metrics, enable the `metrics` feature, this will initialize a `SdkMeterProvider`, set it globally and add a a [`MetricsLayer`](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.MetricsLayer.html) to allow using `tracing` events to produce metrics. The `opentelemetry_sdk` can still be used to produce metrics as well, since we configured the `SdkMeterProvider` globally, so any Axum/Tonic middleware that does not use `tracing` but directly [opentelemetry::metrics](https://docs.rs/opentelemetry/latest/opentelemetry/metrics/struct.Meter.html) will work. Configure the following set of environment variables to configure the metrics exporter (on top of those configured above): - `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` override to `OTEL_EXPORTER_OTLP_ENDPOINT` for the url of the exporter / collector - `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` override to `OTEL_EXPORTER_OTLP_PROTOCOL`, fallback to auto-detection based on ENDPOINT port - `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` to set the timeout for the connection to the exporter - `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` to set the temporality preference for the exporter - `OTEL_METRIC_EXPORT_INTERVAL` to set frequence of metrics export in **_milliseconds_**, defaults to 60s ## Logs To configure OpenTelemetry log export, enable the `logs` feature. This initializes a `SdkLoggerProvider` and adds a log bridge layer so that `tracing` events are forwarded to the OpenTelemetry log pipeline and exported via OTLP. ```toml [dependencies] init-tracing-opentelemetry = { version = "*", features = ["otlp", "logs"] } ``` Standard `tracing` macros emit logs that are automatically bridged: ```rust #[tokio::main] async fn main() -> Result<(), Box> { let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; tracing::error!("This is ground control to Major Tom"); tracing::warn!("Houston, we have a problem"); tracing::info!("We have contact"); tracing::debug!("Roger, copy that"); Ok(()) } ``` Log export can be toggled at runtime via `.with_logs(bool)`: ```rust,no_run use init_tracing_opentelemetry::TracingConfig; //... TracingConfig::default() .with_logs(false) // disable log export (default: enabled when feature is active) .init_subscriber() .expect("valid tracing configuration"); ``` > Traces are automatically attached to logs as well, so if the logs are queried in Grafana (for example), the trace automatically links to the log line. ![screenshot of grafana logs](https://raw.githubusercontent.com/davidB/tracing-opentelemetry-instrumentation-sdk/refs/heads/main/examples/logging/screenshot_grafana.png) Configure the following environment variables to control the logs exporter (in addition to the shared variables above): - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` overrides `OTEL_EXPORTER_OTLP_ENDPOINT` for the log pipeline; for HTTP the path `/v1/logs` is appended automatically - `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` overrides `OTEL_EXPORTER_OTLP_PROTOCOL`, falls back to port-based auto-detection ```sh # For GRPC: export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://localhost:4317" export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="grpc" # For HTTP: export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://127.0.0.1:4318/v1/logs" export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf" ``` > **Note:** A protocol must be set (via env var or inferable from the endpoint port). If neither is found, no log exporter is created and a warning is emitted on target `otel::setup`. ## Changelog - History [CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md) ================================================ FILE: init-tracing-opentelemetry/src/config.rs ================================================ //! Flexible tracing configuration with builder pattern. //! //! Provides [`TracingConfig`] for configurable tracing setup with format options, //! output destinations, level filtering, and OpenTelemetry integration. //! //! # Example //! ```no_run //! use init_tracing_opentelemetry::TracingConfig; //! //! // Use preset with global subscriber (default) //! let _guard = TracingConfig::development().init_subscriber()?; //! //! // Custom configuration with global subscriber //! let _guard = TracingConfig::default() //! .with_json_format() //! .with_stderr() //! .with_log_directives("debug") //! .init_subscriber()?; //! //! // Non-global subscriber (thread-local) //! let guard = TracingConfig::development() //! .with_global_subscriber(false) //! .init_subscriber()?; //! // Guard must be kept alive for subscriber to remain active //! assert!(guard.is_non_global()); //! //! // Without OpenTelemetry (just logging) //! let guard = TracingConfig::minimal() //! .with_otel(false) //! .init_subscriber()?; //! // Works fine - guard.otel_guard is None //! assert!(!guard.has_otel()); //! assert!(guard.otel_guard.is_none()); //! //! // Direct field access is also possible //! if let Some(otel_guard) = &guard.otel_guard { //! // Use otel_guard... //! } //! # Ok::<(), Box>(()) //! ``` #![allow(deprecated)] use std::path::{Path, PathBuf}; use tracing::{Subscriber, info, level_filters::LevelFilter}; use tracing_subscriber::{ Layer, Registry, filter::EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, registry::LookupSpan, }; #[cfg(feature = "logfmt")] use crate::formats::LogfmtLayerBuilder; use crate::formats::{ CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder, }; #[cfg(feature = "logs")] use crate::tracing_subscriber_ext::build_logger_layer_with_resource; #[cfg(feature = "metrics")] use crate::tracing_subscriber_ext::build_metrics_layer_with_resource; use crate::tracing_subscriber_ext::build_tracer_layer_with_resource_and_name; use crate::{Error, otlp::OtelGuard, resource::DetectResource}; /// Combined guard that handles both `OtelGuard` and optional `DefaultGuard` /// /// This struct holds the various guards needed to maintain the tracing subscriber. /// - `otel_guard`: OpenTelemetry guard for flushing traces/metrics on drop (None when OTEL disabled) /// - `default_guard`: Subscriber default guard for non-global subscribers (None when using global) #[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/log/metrics are sent to the server and subscriber is maintained"] pub struct Guard { /// OpenTelemetry guard for proper cleanup (None when OTEL is disabled) pub otel_guard: Option, /// Default subscriber guard for non-global mode (None when using global subscriber) pub default_guard: Option, // Easy to add in the future: // pub log_guard: Option, // pub metrics_guard: Option, } impl Guard { /// Create a new Guard for global subscriber mode pub fn global(otel_guard: Option) -> Self { Self { otel_guard, default_guard: None, } } /// Create a new Guard for non-global subscriber mode pub fn non_global( otel_guard: Option, default_guard: tracing::subscriber::DefaultGuard, ) -> Self { Self { otel_guard, default_guard: Some(default_guard), } } /// Get a reference to the underlying `OtelGuard` if present #[must_use] pub fn otel_guard(&self) -> Option<&OtelGuard> { self.otel_guard.as_ref() } /// Check if OpenTelemetry is enabled for this guard #[must_use] pub fn has_otel(&self) -> bool { self.otel_guard.is_some() } /// Check if this guard is managing a non-global (thread-local) subscriber #[must_use] pub fn is_non_global(&self) -> bool { self.default_guard.is_some() } /// Check if this guard is for a global subscriber #[must_use] pub fn is_global(&self) -> bool { self.default_guard.is_none() } } /// Configuration for log output format #[derive(Debug, Clone)] pub enum LogFormat { /// Pretty formatted output with colors and indentation (development) Pretty, /// Structured JSON output (production) Json, /// Single-line output Full, /// Single-line compact output Compact, /// Key=value logfmt format #[cfg(feature = "logfmt")] Logfmt, } impl Default for LogFormat { fn default() -> Self { if cfg!(debug_assertions) { LogFormat::Pretty } else { LogFormat::Json } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LogTimer { None, Time, Uptime, } impl Default for LogTimer { fn default() -> Self { if cfg!(debug_assertions) { LogTimer::Uptime } else { LogTimer::Time } } } /// Configuration for log output destination #[derive(Debug, Clone, Default)] pub enum WriterConfig { /// Write to stdout #[default] Stdout, /// Write to stderr Stderr, /// Write to a file File(PathBuf), } /// Configuration for log level filtering #[derive(Debug, Clone)] pub struct LevelConfig { /// Log directives string (takes precedence over env vars) pub directives: String, /// Environment variable fallbacks (checked in order) pub env_fallbacks: Vec, /// Default level when no directives or env vars are set pub default_level: LevelFilter, /// OpenTelemetry tracing level pub otel_trace_level: LevelFilter, } impl Default for LevelConfig { fn default() -> Self { Self { directives: String::new(), env_fallbacks: vec!["RUST_LOG".to_string(), "OTEL_LOG_LEVEL".to_string()], default_level: LevelFilter::INFO, otel_trace_level: LevelFilter::TRACE, } } } /// Configuration for optional tracing features #[derive(Debug, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct FeatureSet { /// Include file names in output pub file_names: bool, /// Include line numbers in output pub line_numbers: bool, /// Include thread names in output pub thread_names: bool, /// Include thread IDs in output pub thread_ids: bool, /// Configure time logging (wall clock, uptime or none) pub timer: LogTimer, /// Configure span event logging pub span_events: Option, /// Display target information pub target_display: bool, } impl Default for FeatureSet { fn default() -> Self { Self { file_names: true, line_numbers: cfg!(debug_assertions), thread_names: cfg!(debug_assertions), thread_ids: false, timer: LogTimer::default(), span_events: if cfg!(debug_assertions) { Some(FmtSpan::NEW | FmtSpan::CLOSE) } else { None }, target_display: true, } } } /// Configuration for OpenTelemetry integration #[derive(Debug)] pub struct OtelConfig { /// Enable OpenTelemetry tracing pub enabled: bool, /// Resource configuration for OTEL pub resource_config: Option, /// Enable log export via OTLP pub logs_enabled: bool, /// Enable metrics collection pub metrics_enabled: bool, } impl Default for OtelConfig { fn default() -> Self { Self { enabled: true, resource_config: None, logs_enabled: cfg!(feature = "logs"), metrics_enabled: cfg!(feature = "metrics"), } } } /// Main configuration builder for tracing setup /// Default create a new tracing configuration with sensible defaults #[derive(Debug)] pub struct TracingConfig { /// Output format configuration pub format: LogFormat, /// Output destination configuration pub writer: WriterConfig, /// Level filtering configuration pub level_config: LevelConfig, /// Optional features configuration pub features: FeatureSet, /// OpenTelemetry configuration pub otel_config: OtelConfig, /// Whether to set the subscriber as global default pub global_subscriber: bool, /// name of the tracer (default `""`) pub tracer_name: String, } impl Default for TracingConfig { fn default() -> Self { Self { format: LogFormat::default(), writer: WriterConfig::default(), level_config: LevelConfig::default(), features: FeatureSet::default(), otel_config: OtelConfig::default(), global_subscriber: true, tracer_name: String::new(), } } } impl TracingConfig { // === Format Configuration === /// Set the log format #[must_use] pub fn with_format(mut self, format: LogFormat) -> Self { self.format = format; self } /// Use pretty formatted output (development style) #[must_use] pub fn with_pretty_format(self) -> Self { self.with_format(LogFormat::Pretty) } /// Use JSON formatted output (production style) #[must_use] pub fn with_json_format(self) -> Self { self.with_format(LogFormat::Json) } /// Use full formatted output #[must_use] pub fn with_full_format(self) -> Self { self.with_format(LogFormat::Full) } /// Use compact formatted output #[must_use] pub fn with_compact_format(self) -> Self { self.with_format(LogFormat::Compact) } /// Use logfmt formatted output (requires 'logfmt' feature) #[must_use] #[cfg(feature = "logfmt")] pub fn with_logfmt_format(self) -> Self { self.with_format(LogFormat::Logfmt) } // === Writer Configuration === /// Set the output writer #[must_use] pub fn with_writer(mut self, writer: WriterConfig) -> Self { self.writer = writer; self } /// Write logs to stdout #[must_use] pub fn with_stdout(self) -> Self { self.with_writer(WriterConfig::Stdout) } /// Write logs to stderr #[must_use] pub fn with_stderr(self) -> Self { self.with_writer(WriterConfig::Stderr) } /// Write logs to a file #[must_use] pub fn with_file>(self, path: P) -> Self { self.with_writer(WriterConfig::File(path.as_ref().to_path_buf())) } // === Level Configuration === /// Set log directives (takes precedence over environment variables), /// for example if you want to set it from cli arguments (verbosity) #[must_use] pub fn with_log_directives(mut self, directives: impl Into) -> Self { self.level_config.directives = directives.into(); self } /// Set the default log level #[must_use] pub fn with_default_level(mut self, level: LevelFilter) -> Self { self.level_config.default_level = level; self } /// Add an environment variable fallback for log configuration #[must_use] pub fn with_env_fallback(mut self, env_var: impl Into) -> Self { self.level_config.env_fallbacks.push(env_var.into()); self } /// Set the OpenTelemetry trace level #[must_use] pub fn with_otel_trace_level(mut self, level: LevelFilter) -> Self { self.level_config.otel_trace_level = level; self } /// Set the name of the tracer (default `""`) #[must_use] pub fn with_otel_tracer_name(mut self, name: impl Into) -> Self { self.tracer_name = name.into(); self } // === Feature Configuration === /// Enable or disable file names in output #[must_use] pub fn with_file_names(mut self, enabled: bool) -> Self { self.features.file_names = enabled; self } /// Enable or disable line numbers in output #[must_use] pub fn with_line_numbers(mut self, enabled: bool) -> Self { self.features.line_numbers = enabled; self } /// Enable or disable thread names in output #[must_use] pub fn with_thread_names(mut self, enabled: bool) -> Self { self.features.thread_names = enabled; self } /// Enable or disable thread IDs in output #[must_use] pub fn with_thread_ids(mut self, enabled: bool) -> Self { self.features.thread_ids = enabled; self } /// Configure span event logging #[must_use] pub fn with_span_events(mut self, events: FmtSpan) -> Self { self.features.span_events = Some(events); self } /// Disable span event logging #[must_use] pub fn without_span_events(mut self) -> Self { self.features.span_events = None; self } /// Enable or disable uptime timer (vs wall clock) #[must_use] #[deprecated = "Use `TracingConfig::with_timer` instead"] pub fn with_uptime_timer(mut self, enabled: bool) -> Self { self.features.timer = if enabled { LogTimer::Uptime } else { LogTimer::Time }; self } /// Configure time logging (wall clock, uptime or none) #[must_use] pub fn with_timer(mut self, timer: LogTimer) -> Self { self.features.timer = timer; self } /// Enable or disable target display #[must_use] pub fn with_target_display(mut self, enabled: bool) -> Self { self.features.target_display = enabled; self } // === OpenTelemetry Configuration === /// Enable or disable OpenTelemetry tracing #[must_use] pub fn with_otel(mut self, enabled: bool) -> Self { self.otel_config.enabled = enabled; self } /// Enable or disable log export via OTLP #[must_use] pub fn with_logs(mut self, enabled: bool) -> Self { self.otel_config.logs_enabled = enabled; self } /// Enable or disable metrics collection #[must_use] pub fn with_metrics(mut self, enabled: bool) -> Self { self.otel_config.metrics_enabled = enabled; self } /// Set resource configuration for OpenTelemetry #[must_use] pub fn with_resource_config(mut self, config: DetectResource) -> Self { self.otel_config.resource_config = Some(config); self } /// Set whether to initialize the subscriber as global default /// /// When `global` is true (default), the subscriber is set as the global default. /// When false, the subscriber is set as thread-local default and the returned /// Guard must be kept alive to maintain the subscriber. #[must_use] pub fn with_global_subscriber(mut self, global: bool) -> Self { self.global_subscriber = global; self } // === Build Methods === /// Build a tracing layer with the current configuration pub fn build_layer(&self) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { match &self.format { LogFormat::Pretty => PrettyLayerBuilder.build_layer(self), LogFormat::Json => JsonLayerBuilder.build_layer(self), LogFormat::Full => FullLayerBuilder.build_layer(self), LogFormat::Compact => CompactLayerBuilder.build_layer(self), #[cfg(feature = "logfmt")] LogFormat::Logfmt => LogfmtLayerBuilder.build_layer(self), } } /// Build a level filter layer with the current configuration pub fn build_filter_layer(&self) -> Result { // Use existing function but with our configuration let dirs = if self.level_config.directives.is_empty() { // Try environment variables in order self.level_config .env_fallbacks .iter() .find_map(|var| std::env::var(var).ok()) .unwrap_or_else(|| self.level_config.default_level.to_string().to_lowercase()) } else { self.level_config.directives.clone() }; let directive_to_allow_otel_trace = format!( "otel::tracing={}", self.level_config .otel_trace_level .to_string() .to_lowercase() ) .parse()?; Ok(EnvFilter::builder() .with_default_directive(self.level_config.default_level.into()) .parse_lossy(dirs) .add_directive(directive_to_allow_otel_trace)) } /// Initialize the tracing subscriber with this configuration /// /// If `global_subscriber` is true, sets the subscriber as the global default. /// If false, returns a Guard that maintains the subscriber as the thread-local default. /// /// When OpenTelemetry is disabled, the Guard will contain `None` for the `OtelGuard`. pub fn init_subscriber(self) -> Result { self.init_subscriber_ext(Self::transform_identity) } fn transform_identity(s: Registry) -> Registry { s } /// `transform` parameter allow to customize the registry/subscriber before /// the setup of opentelemetry, log, logfilter. /// ```text /// let guard = TracingConfig::default() /// .with_json_format() /// .with_stderr() /// .init_subscriber_ext(|subscriber| subscriber.with(my_layer))?; /// ``` pub fn init_subscriber_ext(self, transform: F) -> Result where SOut: Subscriber + for<'a> LookupSpan<'a> + Send + Sync, F: FnOnce(Registry) -> SOut, { // Setup a temporary subscriber for initialization logging let temp_subscriber = tracing_subscriber::registry() .with(self.build_layer()?) .with(self.build_filter_layer()?); let _guard = tracing::subscriber::set_default(temp_subscriber); info!("init logging & tracing"); // Build the final subscriber based on OTEL configuration if self.otel_config.enabled { let subscriber = transform(tracing_subscriber::registry()); let layer = self.build_layer()?; let filter_layer = self.build_filter_layer()?; let (subscriber, otel_guard) = self.register_otel_layers_with_resource(subscriber)?; let subscriber = subscriber.with(layer).with(filter_layer); if self.global_subscriber { tracing::subscriber::set_global_default(subscriber)?; Ok(Guard::global(Some(otel_guard))) } else { let default_guard = tracing::subscriber::set_default(subscriber); Ok(Guard::non_global(Some(otel_guard), default_guard)) } } else { info!("OpenTelemetry disabled - proceeding without OTEL layers"); let subscriber = transform(tracing_subscriber::registry()) .with(self.build_layer()?) .with(self.build_filter_layer()?); if self.global_subscriber { tracing::subscriber::set_global_default(subscriber)?; Ok(Guard::global(None)) } else { let default_guard = tracing::subscriber::set_default(subscriber); Ok(Guard::non_global(None, default_guard)) } } } fn register_otel_layers_with_resource( &self, subscriber: S, ) -> Result<(impl Subscriber + for<'span> LookupSpan<'span>, OtelGuard), Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let otel_rsrc = self .otel_config .resource_config .clone() .unwrap_or_default() .build(); #[cfg(feature = "logs")] let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?; #[cfg(feature = "metrics")] let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?; let (trace_layer, tracer_provider) = build_tracer_layer_with_resource_and_name(otel_rsrc, self.tracer_name.clone())?; let subscriber = subscriber.with(trace_layer); #[cfg(feature = "logs")] let subscriber = subscriber.with(self.otel_config.logs_enabled.then_some(logs_layer)); #[cfg(feature = "metrics")] let subscriber = subscriber.with(self.otel_config.metrics_enabled.then_some(metrics_layer)); Ok(( subscriber, OtelGuard { #[cfg(feature = "logs")] logger_provider, #[cfg(feature = "metrics")] meter_provider, tracer_provider, }, )) } // === Preset Configurations === /// Configuration preset for development environments /// - Pretty formatting with colors /// - Output to stderr /// - Line numbers and thread names enabled /// - Span events for NEW and CLOSE /// - Full OpenTelemetry integration #[must_use] pub fn development() -> Self { Self::default() .with_pretty_format() .with_stderr() .with_line_numbers(true) .with_thread_names(true) .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) .with_otel(true) } /// Configuration preset for production environments /// - JSON formatting for structured logging /// - Output to stdout /// - Minimal metadata (no line numbers or thread names) /// - No span events to reduce verbosity /// - Full OpenTelemetry integration #[must_use] pub fn production() -> Self { Self::default() .with_json_format() .with_stdout() .with_line_numbers(false) .with_thread_names(false) .without_span_events() .with_otel(true) } /// Configuration preset for debugging /// - Pretty formatting with full verbosity /// - Output to stderr /// - All metadata enabled /// - Full span events /// - Debug level logging /// - Full OpenTelemetry integration #[must_use] pub fn debug() -> Self { Self::development() .with_log_directives("debug") .with_span_events(FmtSpan::FULL) .with_target_display(true) } /// Configuration preset for minimal logging /// - Compact formatting /// - Output to stdout /// - No metadata or extra features /// - OpenTelemetry disabled for minimal overhead #[must_use] pub fn minimal() -> Self { Self::default() .with_compact_format() .with_stdout() .with_line_numbers(false) .with_thread_names(false) .without_span_events() .with_target_display(false) .with_otel(false) } /// Configuration preset for testing environments /// - Compact formatting for less noise /// - Output to stderr to separate from test output /// - Basic metadata /// - OpenTelemetry disabled for speed /// - non global registration (of subscriber) #[must_use] pub fn testing() -> Self { Self::default() .with_compact_format() .with_stderr() .with_line_numbers(false) .with_thread_names(false) .without_span_events() .with_otel(false) .with_global_subscriber(false) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_global_subscriber_true_returns_global_guard() { let config = TracingConfig::minimal() .with_global_subscriber(true) .with_otel(false); // Disable for simple test // This would actually initialize the subscriber, so we'll just test that // the config has the right value assert!(config.global_subscriber); } #[test] fn test_global_subscriber_false_sets_config() { let config = TracingConfig::minimal() .with_global_subscriber(false) .with_otel(false); // Disable for simple test assert!(!config.global_subscriber); } #[test] fn test_default_global_subscriber_is_true() { let config = TracingConfig::default(); assert!(config.global_subscriber); } #[test] fn test_init_subscriber_without_otel_succeeds() { // Test that initialization succeeds when OTEL is disabled let guard = TracingConfig::minimal() .with_otel(false) .with_global_subscriber(false) // Use non-global to avoid affecting other tests .init_subscriber(); assert!(guard.is_ok()); let guard = guard.unwrap(); // Verify that the guard indicates no OTEL assert!(!guard.has_otel()); assert!(guard.otel_guard().is_none()); } #[test] fn test_init_subscriber_with_otel_disabled_global() { // Test global subscriber mode with OTEL disabled let guard = TracingConfig::minimal() .with_otel(false) .with_global_subscriber(true) .init_subscriber(); assert!(guard.is_ok()); let guard = guard.unwrap(); // Should be global mode with no OTEL assert!(guard.is_global()); assert!(!guard.has_otel()); assert!(guard.otel_guard().is_none()); } #[test] fn test_init_subscriber_with_otel_disabled_non_global() { // Test non-global subscriber mode with OTEL disabled let guard = TracingConfig::minimal() .with_otel(false) .with_global_subscriber(false) .init_subscriber(); assert!(guard.is_ok()); let guard = guard.unwrap(); // Should be non-global mode with no OTEL assert!(guard.is_non_global()); assert!(!guard.has_otel()); assert!(guard.otel_guard().is_none()); } #[test] fn test_guard_helper_methods() { // Test the Guard helper methods work correctly with None values let guard_global_none = Guard::global(None); assert!(!guard_global_none.has_otel()); assert!(guard_global_none.otel_guard().is_none()); assert!(guard_global_none.is_global()); assert!(!guard_global_none.is_non_global()); assert!(guard_global_none.default_guard.is_none()); // We can't easily create a DefaultGuard for testing, but we can test the constructor // Note: We can't actually create a DefaultGuard without setting up a real subscriber, // so we'll just test the struct design is sound } #[test] fn test_guard_struct_direct_field_access() { // Test that we can directly access fields, which is a benefit of the struct design let guard = Guard::global(None); // Direct field access is now possible assert!(guard.otel_guard.is_none()); assert!(guard.default_guard.is_none()); // Helper methods still work assert!(!guard.has_otel()); assert!(guard.is_global()); } #[test] fn test_guard_struct_extensibility() { // This test demonstrates how the struct design makes it easier to extend // We can easily add more optional guards in the future without breaking existing code let guard = Guard { otel_guard: None, default_guard: None, // Future: log_guard: None, metrics_guard: None, etc. }; assert!(guard.is_global()); assert!(!guard.has_otel()); } #[tokio::test] async fn test_init_with_transform() { use std::time::Duration; use tokio_blocked::TokioBlockedLayer; let blocked = TokioBlockedLayer::new().with_warn_busy_single_poll(Some(Duration::from_micros(150))); let guard = TracingConfig::default() .with_json_format() .with_stderr() .with_log_directives("debug") .with_global_subscriber(false) .init_subscriber_ext(|subscriber| subscriber.with(blocked)) .unwrap(); assert!(!guard.is_global()); assert!(guard.has_otel()); } } ================================================ FILE: init-tracing-opentelemetry/src/error.rs ================================================ #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] SetGlobalDefaultError(#[from] tracing::subscriber::SetGlobalDefaultError), #[error(transparent)] TraceError(#[from] opentelemetry_sdk::trace::TraceError), #[cfg(feature = "otlp")] #[error(transparent)] ExporterBuildError(#[from] opentelemetry_otlp::ExporterBuildError), #[cfg(feature = "tracing_subscriber_ext")] #[error(transparent)] FilterParseError(#[from] tracing_subscriber::filter::ParseError), } ================================================ FILE: init-tracing-opentelemetry/src/formats.rs ================================================ //! Format-specific layer builders for tracing output. //! //! Provides implementations for different log formats (Pretty, JSON, Compact, Logfmt) //! using the strategy pattern with the [`LayerBuilder`] trait. use tracing::Subscriber; use tracing_subscriber::fmt; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::fmt::time::{Uptime, time, uptime}; use tracing_subscriber::{Layer, registry::LookupSpan}; use crate::config::{LogTimer, TracingConfig, WriterConfig}; use crate::{Error, FeatureSet}; /// Trait for building format-specific tracing layers pub trait LayerBuilder: Send + Sync { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>; } fn configure_layer( mut layer: fmt::Layer, W>, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'writer> fmt::FormatFields<'writer> + Send + Sync + 'static, L: Send + Sync + 'static, fmt::format::Format: fmt::FormatEvent, fmt::format::Format: fmt::FormatEvent, fmt::format::Format: fmt::FormatEvent, W: for<'writer> fmt::MakeWriter<'writer> + Send + Sync + 'static, { // NOTE: Destructure to make sure we don’t miss a feature let FeatureSet { file_names, line_numbers, thread_names, thread_ids, timer, span_events, target_display, } = &config.features; let span_events = span_events .as_ref() .map_or(FmtSpan::NONE, ToOwned::to_owned); // Configure features layer = layer .with_file(*file_names) .with_line_number(*line_numbers) .with_thread_names(*thread_names) .with_thread_ids(*thread_ids) .with_span_events(span_events) .with_target(*target_display); // Configure timer and writer match timer { LogTimer::None => configure_writer(layer.without_time(), &config.writer), LogTimer::Time => configure_writer(layer.with_timer(time()), &config.writer), LogTimer::Uptime => configure_writer(layer.with_timer(uptime()), &config.writer), } } fn configure_writer( layer: fmt::Layer, W>, writer: &WriterConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'writer> fmt::FormatFields<'writer> + Send + Sync + 'static, L: Send + Sync + 'static, T: Send + Sync + 'static, fmt::format::Format: fmt::FormatEvent, { match writer { WriterConfig::Stdout => Ok(Box::new(layer.with_writer(std::io::stdout))), WriterConfig::Stderr => Ok(Box::new(layer.with_writer(std::io::stderr))), WriterConfig::File(path) => { let file = std::fs::OpenOptions::new() .create(true) .append(true) .open(path)?; Ok(Box::new(layer.with_writer(file))) } } } /// Builder for pretty-formatted logs (development style) #[derive(Debug, Default, Clone)] pub struct PrettyLayerBuilder; impl LayerBuilder for PrettyLayerBuilder { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let layer = tracing_subscriber::fmt::layer().pretty(); configure_layer(layer, config) } } /// Builder for JSON-formatted logs (production style) #[derive(Debug, Default, Clone)] pub struct JsonLayerBuilder; impl LayerBuilder for JsonLayerBuilder { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let layer = tracing_subscriber::fmt::layer().json(); configure_layer(layer, config) } } /// Builder for full-formatted logs (default `tracing` style) #[derive(Debug, Default, Clone)] pub struct FullLayerBuilder; impl LayerBuilder for FullLayerBuilder { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let layer = tracing_subscriber::fmt::layer(); configure_layer(layer, config) } } /// Builder for compact-formatted logs (minimal style) #[derive(Debug, Default, Clone)] pub struct CompactLayerBuilder; impl LayerBuilder for CompactLayerBuilder { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let layer = tracing_subscriber::fmt::layer().compact(); configure_layer(layer, config) } } /// Builder for logfmt-formatted logs #[cfg(feature = "logfmt")] #[derive(Debug, Default, Clone)] pub struct LogfmtLayerBuilder; #[cfg(feature = "logfmt")] impl LayerBuilder for LogfmtLayerBuilder { fn build_layer( &self, config: &TracingConfig, ) -> Result + Send + Sync + 'static>, Error> where S: Subscriber + for<'a> LookupSpan<'a>, { // Note: tracing_logfmt doesn't support the same configuration options // as the standard fmt layer, so we have limited configuration ability if let WriterConfig::Stdout = &config.writer { // Default behavior uses stdout Ok(Box::new(tracing_logfmt::layer())) } else { // For stderr, we need to use the builder pattern since layer() doesn't support with_writer // However, the current tracing_logfmt version may not support this // For now, we'll fall back to the basic layer tracing::warn!("logfmt only support stdout"); Ok(Box::new(tracing_logfmt::layer())) } } } ================================================ FILE: init-tracing-opentelemetry/src/lib.rs ================================================ //#![warn(missing_docs)] #![forbid(unsafe_code)] #![warn(clippy::perf)] #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] #![doc = include_str!("../README.md")] #![cfg_attr(docs_rs, feature(doc_cfg))] pub use opentelemetry_sdk; pub use tracing_opentelemetry; mod error; pub use error::Error; use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator}; use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator}; use opentelemetry_sdk::trace::TraceError; #[cfg(feature = "tracing_subscriber_ext")] #[cfg_attr(docs_rs, doc(cfg(feature = "tracing_subscriber_ext")))] pub mod config; #[cfg(feature = "tracing_subscriber_ext")] #[cfg_attr(docs_rs, doc(cfg(feature = "tracing_subscriber_ext")))] pub mod formats; #[cfg(feature = "otlp")] #[cfg_attr(docs_rs, doc(cfg(feature = "otlp")))] pub mod otlp; #[cfg(feature = "tracer")] #[cfg_attr(docs_rs, doc(cfg(feature = "tracer")))] pub mod resource; #[cfg(feature = "stdout")] #[cfg_attr(docs_rs, doc(cfg(feature = "stdout")))] pub mod stdio; #[cfg(feature = "tracing_subscriber_ext")] #[cfg_attr(docs_rs, doc(cfg(feature = "tracing_subscriber_ext")))] pub mod tracing_subscriber_ext; /// Configure the global propagator based on content of the env variable [OTEL_PROPAGATORS](https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/#otel_propagators) /// Specifies Propagators to be used in a comma-separated list. /// Default value: `"tracecontext,baggage"` /// Example: `export OTEL_PROPAGATORS="b3"` /// Accepted values for `OTEL_PROPAGATORS` are: /// /// - "tracecontext": W3C Trace Context /// - "baggage": W3C Baggage /// - "b3": B3 Single (require feature "zipkin") /// - "b3multi": B3 Multi (require feature "zipkin") /// - "jaeger": Jaeger (require feature "jaeger") /// - "xray": AWS X-Ray (require feature "xray") /// - "ottrace": OT Trace (third party) (not supported) /// - "none": No automatically configured propagator. /// /// # Errors /// /// Will return `TraceError` if issue in reading or instanciate propagator. pub fn init_propagator() -> Result<(), TraceError> { let value_from_env = std::env::var("OTEL_PROPAGATORS").unwrap_or_else(|_| "tracecontext,baggage".to_string()); let propagators: Vec<(Box, String)> = value_from_env .split(',') .map(|s| { let name = s.trim().to_lowercase(); propagator_from_string(&name).map(|o| o.map(|b| (b, name))) }) .collect::, _>>()? .into_iter() .flatten() .collect(); if !propagators.is_empty() { let (propagators_impl, propagators_name): (Vec<_>, Vec<_>) = propagators.into_iter().unzip(); tracing::debug!(target: "otel::setup", OTEL_PROPAGATORS = propagators_name.join(",")); let composite_propagator = TextMapCompositePropagator::new(propagators_impl); opentelemetry::global::set_text_map_propagator(composite_propagator); } Ok(()) } #[allow(clippy::box_default)] fn propagator_from_string( v: &str, ) -> Result>, TraceError> { match v { "tracecontext" => Ok(Some(Box::new(TraceContextPropagator::new()))), "baggage" => Ok(Some(Box::new(BaggagePropagator::new()))), #[cfg(feature = "zipkin")] "b3" => Ok(Some(Box::new( opentelemetry_zipkin::Propagator::with_encoding( opentelemetry_zipkin::B3Encoding::SingleHeader, ), ))), #[cfg(not(feature = "zipkin"))] "b3" => Err(TraceError::from( "unsupported propagators form env OTEL_PROPAGATORS: 'b3', try to enable compile feature 'zipkin'", )), #[cfg(feature = "zipkin")] "b3multi" => Ok(Some(Box::new( opentelemetry_zipkin::Propagator::with_encoding( opentelemetry_zipkin::B3Encoding::MultipleHeader, ), ))), #[cfg(not(feature = "zipkin"))] "b3multi" => Err(TraceError::from( "unsupported propagators form env OTEL_PROPAGATORS: 'b3multi', try to enable compile feature 'zipkin'", )), #[cfg(feature = "jaeger")] "jaeger" => Ok(Some(Box::new( opentelemetry_jaeger_propagator::Propagator::default(), ))), #[cfg(not(feature = "jaeger"))] "jaeger" => Err(TraceError::from( "unsupported propagators form env OTEL_PROPAGATORS: 'jaeger', try to enable compile feature 'jaeger'", )), #[cfg(feature = "xray")] "xray" => Ok(Some(Box::new( opentelemetry_aws::trace::XrayPropagator::default(), ))), #[cfg(not(feature = "xray"))] "xray" => Err(TraceError::from( "unsupported propagators form env OTEL_PROPAGATORS: 'xray', try to enable compile feature 'xray'", )), "none" => Ok(None), unknown => Err(TraceError::from(format!( "unsupported propagators form env OTEL_PROPAGATORS: '{unknown}'" ))), } } // Re-export the new configuration API for easier access #[cfg(feature = "tracing_subscriber_ext")] pub use config::{ FeatureSet, Guard, LevelConfig, LogFormat, LogTimer, OtelConfig, TracingConfig, WriterConfig, }; #[cfg(feature = "tracing_subscriber_ext")] pub use formats::{ CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder, }; #[cfg(all(feature = "tracing_subscriber_ext", feature = "logfmt"))] pub use formats::LogfmtLayerBuilder; #[cfg(test)] #[cfg(feature = "tracer")] mod tests { use assert2::assert; #[test] fn init_tracing_failed_on_invalid_propagator() { assert!(let Err(_) = super::propagator_from_string("xxxxxx")); // std::env::set_var("OTEL_PROPAGATORS", "xxxxxx"); // dbg!(std::env::var("OTEL_PROPAGATORS")); // assert!(let Err(_) = init_tracing()); } } ================================================ FILE: init-tracing-opentelemetry/src/otlp/logs.rs ================================================ use super::infer_protocol_from_env; use opentelemetry_otlp::{ExporterBuildError, LogExporter}; use opentelemetry_sdk::{Resource, logs::LoggerProviderBuilder, logs::SdkLoggerProvider}; #[cfg(feature = "tls")] use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig}; #[must_use] pub fn identity(v: LoggerProviderBuilder) -> LoggerProviderBuilder { v } pub fn init_loggerprovider( resource: Resource, transform: F, ) -> Result where F: FnOnce(LoggerProviderBuilder) -> LoggerProviderBuilder, { let protocol = infer_protocol_from_env( "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "v1/logs", ); // builders used the environment variables to determine the endpoint (but not to setup the protocol) let exporter: Option = match protocol.as_deref() { Some("http/protobuf") => Some(LogExporter::builder().with_http().build()?), #[cfg(feature = "tls")] Some("grpc/tls") => Some( LogExporter::builder() .with_tonic() .with_tls_config(ClientTlsConfig::new().with_enabled_roots()) .build()?, ), Some("grpc") => Some(LogExporter::builder().with_tonic().build()?), Some(x) => { tracing::warn!( "unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created" ); None } None => { tracing::warn!( "no env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created" ); None } }; let mut logger_provider = SdkLoggerProvider::builder().with_resource(resource); if let Some(exporter) = exporter { logger_provider = logger_provider.with_batch_exporter(exporter); } logger_provider = transform(logger_provider); Ok(logger_provider.build()) } ================================================ FILE: init-tracing-opentelemetry/src/otlp/metrics.rs ================================================ use super::infer_protocol_from_env; use opentelemetry_otlp::{ExporterBuildError, MetricExporter, WithExportConfig}; use opentelemetry_sdk::Resource; use opentelemetry_sdk::metrics::{ MeterProviderBuilder, PeriodicReader, SdkMeterProvider, Temporality, }; use std::env; use std::time::Duration; #[cfg(feature = "tls")] use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig}; #[must_use] pub fn identity(v: MeterProviderBuilder) -> MeterProviderBuilder { v } pub fn init_meterprovider( resource: Resource, transform: F, ) -> Result where F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder, { let protocol = infer_protocol_from_env( "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "v1/metrics", ); let timeout = env::var("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT") .ok() .and_then(|var| var.parse::().ok()) .map_or(Duration::from_secs(10), Duration::from_secs); let temporality = env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") .ok() .and_then(|var| match var.to_lowercase().as_str() { "delta" => Some(Temporality::Delta), "cumulative" => Some(Temporality::Cumulative), unknown => { tracing::warn!("unknown '{unknown}' env var set for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY; defaulting to cumulative"); None }, }) .unwrap_or_default(); let export_interval = env::var("OTEL_METRIC_EXPORT_INTERVAL") .ok() .and_then(|var| var.parse::().ok()) .map_or(Duration::from_secs(60), Duration::from_millis); // builders used the environment variables to determine the endpoint (but not to setup the protocol) let exporter = match protocol.as_deref() { Some("http/protobuf") => Some( MetricExporter::builder() .with_http() .with_temporality(temporality) .with_timeout(timeout) .build()?, ), #[cfg(feature = "tls")] Some("grpc/tls") => Some( MetricExporter::builder() .with_tonic() .with_tls_config(ClientTlsConfig::new().with_enabled_roots()) .with_temporality(temporality) .with_timeout(timeout) .build()?, ), Some("grpc") => Some( MetricExporter::builder() .with_tonic() .with_temporality(temporality) .with_timeout(timeout) .build()?, ), Some(x) => { tracing::warn!( "unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no metric exporter will be created" ); None } None => { tracing::warn!( "no env var set or infered for OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no metric exporter will be created" ); None } }; let mut meter_provider = SdkMeterProvider::builder().with_resource(resource); if let Some(exporter) = exporter { let reader = PeriodicReader::builder(exporter) .with_interval(export_interval) .build(); meter_provider = meter_provider.with_reader(reader); } meter_provider = transform(meter_provider); Ok(meter_provider.build()) } ================================================ FILE: init-tracing-opentelemetry/src/otlp/mod.rs ================================================ #[cfg(feature = "logs")] pub mod logs; #[cfg(feature = "metrics")] pub mod metrics; pub mod traces; #[cfg(feature = "logs")] use opentelemetry::logs::LoggerProvider; #[cfg(feature = "metrics")] use opentelemetry::metrics::MeterProvider; #[cfg(feature = "logs")] use opentelemetry_sdk::logs::SdkLoggerProvider; #[cfg(feature = "metrics")] use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry::trace::TracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; #[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/logs/metrics are sent to the server"] /// On Drop of the `OtelGuard` instance, /// the wrapped Tracer/Logger/Meter Provider is force to flush and to shutdown (ignoring error). #[allow(clippy::struct_field_names)] pub struct OtelGuard { #[cfg(feature = "logs")] pub(crate) logger_provider: SdkLoggerProvider, #[cfg(feature = "metrics")] pub(crate) meter_provider: SdkMeterProvider, pub(crate) tracer_provider: SdkTracerProvider, } impl OtelGuard { #[cfg(feature = "logs")] #[must_use] pub fn logger_provider(&self) -> &impl LoggerProvider { &self.logger_provider } #[must_use] pub fn tracer_provider(&self) -> &impl TracerProvider { &self.tracer_provider } #[cfg(feature = "metrics")] #[must_use] pub fn meter_provider(&self) -> &impl MeterProvider { &self.meter_provider } } impl Drop for OtelGuard { #[allow(unused_must_use)] fn drop(&mut self) { let _ = self.tracer_provider.force_flush(); let _ = self.tracer_provider.shutdown(); #[cfg(feature = "logs")] { let _ = self.logger_provider.force_flush(); let _ = self.logger_provider.shutdown(); } #[cfg(feature = "metrics")] { let _ = self.meter_provider.force_flush(); let _ = self.meter_provider.shutdown(); } } } #[allow(unused_mut)] pub(crate) fn infer_protocol_from_env( protocol_key: &str, endpoint_key: &str, endpoint_path: &str, ) -> Option { let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env(protocol_key, endpoint_key, endpoint_path); infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref()) } #[allow(unused_mut)] fn infer_protocol(maybe_protocol: Option<&str>, maybe_endpoint: Option<&str>) -> Option { let mut maybe_protocol = match (maybe_protocol, maybe_endpoint) { (Some(protocol), _) => Some(protocol.to_string()), (None, Some(endpoint)) => { if endpoint.contains(":4317") { Some("grpc".to_string()) } else { Some("http/protobuf".to_string()) } } _ => None, }; #[cfg(feature = "tls")] if maybe_protocol.as_deref() == Some("grpc") && maybe_endpoint.is_some_and(|e| e.starts_with("https")) { maybe_protocol = Some("grpc/tls".to_string()); } maybe_protocol } pub fn debug_env() { const SENSITIVE_KEYS: &[&str] = &[ "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_EXPORTER_OTLP_CERTIFICATE", ]; std::env::vars() .filter(|(k, _)| k.starts_with("OTEL_")) .for_each(|(k, v)| { let display_value = if SENSITIVE_KEYS.iter().any(|s| k == *s) { "[redacted]" } else { &v }; tracing::debug!(target: "otel::setup::env", key = %k, value = %display_value); }); } fn read_protocol_and_endpoint_from_env( protocol_key: &str, endpoint_key: &str, endpoint_path: &str, ) -> (Option, Option) { let maybe_protocol = std::env::var(protocol_key) .or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL")) .ok(); let maybe_endpoint = std::env::var(endpoint_key) .or_else(|_| { std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol { Some(protocol) if protocol.contains("http") => { format!("{endpoint}/{endpoint_path}") } _ => endpoint, }) }) .ok(); (maybe_protocol, maybe_endpoint) } #[cfg(test)] mod tests { use assert2::assert; use rstest::rstest; use super::*; #[rstest] #[case(None, None, None)] //Devskim: ignore DS137138 #[case(Some("http/protobuf"), None, Some("http/protobuf"))] //Devskim: ignore DS137138 #[case(Some("grpc"), None, Some("grpc"))] //Devskim: ignore DS137138 #[case(None, Some("http://localhost:4317"), Some("grpc"))] //Devskim: ignore DS137138 #[cfg_attr( feature = "tls", case(None, Some("https://localhost:4317"), Some("grpc/tls")) )] #[cfg_attr( feature = "tls", case(Some("grpc/tls"), Some("https://localhost:4317"), Some("grpc/tls")) )] #[case( Some("http/protobuf"), Some("http://localhost:4318/v1/traces"), //Devskim: ignore DS137138 Some("http/protobuf"), )] #[case( Some("http/protobuf"), Some("https://examples.com:4318/v1/traces"), Some("http/protobuf") )] #[case( Some("http/protobuf"), Some("https://examples.com:4317"), Some("http/protobuf") )] fn test_infer_protocol( #[case] traces_protocol: Option<&str>, #[case] traces_endpoint: Option<&str>, #[case] expected_protocol: Option<&str>, ) { assert!(infer_protocol(traces_protocol, traces_endpoint).as_deref() == expected_protocol); } } ================================================ FILE: init-tracing-opentelemetry/src/otlp/traces.rs ================================================ use super::infer_protocol_from_env; use opentelemetry_otlp::{ExporterBuildError, SpanExporter}; use opentelemetry_sdk::{Resource, trace::SdkTracerProvider, trace::TracerProviderBuilder}; #[cfg(feature = "tls")] use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig}; #[must_use] pub fn identity(v: TracerProviderBuilder) -> TracerProviderBuilder { v } // see https://opentelemetry.io/docs/reference/specification/protocol/exporter/ pub fn init_tracerprovider( resource: Resource, transform: F, ) -> Result where F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder, { super::debug_env(); let protocol = infer_protocol_from_env( "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "v1/traces", ); // builders used the environment variables to determine the endpoint (but not to setup the protocol) let exporter: Option = match protocol.as_deref() { Some("http/protobuf") => Some(SpanExporter::builder().with_http().build()?), #[cfg(feature = "tls")] Some("grpc/tls") => Some( SpanExporter::builder() .with_tonic() .with_tls_config(ClientTlsConfig::new().with_enabled_roots()) .build()?, ), Some("grpc") => Some(SpanExporter::builder().with_tonic().build()?), Some(x) => { tracing::warn!( "unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no span exporter will be created" ); None } None => { tracing::warn!( "no env var set or infered for OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no span exporter will be created" ); None } }; let mut trace_provider = SdkTracerProvider::builder().with_resource(resource); if let Some(exporter) = exporter { trace_provider = trace_provider.with_batch_exporter(exporter); } trace_provider = transform(trace_provider); Ok(trace_provider.build()) } ================================================ FILE: init-tracing-opentelemetry/src/resource.rs ================================================ use std::borrow::Cow; use opentelemetry::KeyValue; // use opentelemetry_resource_detectors::OsResourceDetector; use opentelemetry_sdk::{Resource, resource::ResourceDetector}; use opentelemetry_semantic_conventions::resource; /// To log detected value set environement variable `RUST_LOG="...,otel::setup::resource=debug"` /// ```rust /// use init_tracing_opentelemetry::resource::DetectResource; /// # fn main() { /// let otel_rsrc = DetectResource::default() /// .with_fallback_service_name(env!("CARGO_PKG_NAME")) /// .with_fallback_service_version(env!("CARGO_PKG_VERSION")) /// .build(); /// # } /// /// ``` #[derive(Debug, Default, Clone)] pub struct DetectResource { fallback_service_name: Option>, fallback_service_version: Option>, } impl DetectResource { /// `service.name` is first extracted from environment variables /// (in this order) `OTEL_SERVICE_NAME`, `SERVICE_NAME`, `APP_NAME`. /// But a default value can be provided with this method. #[must_use] pub fn with_fallback_service_name( mut self, fallback_service_name: impl Into>, ) -> Self { self.fallback_service_name = Some(fallback_service_name.into()); self } /// `service.name` is first extracted from environment variables /// (in this order) `SERVICE_VERSION`, `APP_VERSION`. /// But a default value can be provided with this method. #[must_use] pub fn with_fallback_service_version( mut self, fallback_service_version: impl Into>, ) -> Self { self.fallback_service_version = Some(fallback_service_version.into()); self } #[must_use] pub fn build(self) -> Resource { //Box::new(OsResourceDetector), //FIXME enable when available for opentelemetry >= 0.25 //Box::new(ProcessResourceDetector), let rsrc = Resource::builder() .with_detector(Box::new(ServiceInfoDetector { fallback_service_name: self.fallback_service_name, fallback_service_version: self.fallback_service_version, })) .build(); debug_resource(&rsrc); rsrc } } pub fn debug_resource(rsrc: &Resource) { rsrc.iter().for_each( |kv| tracing::debug!(target: "otel::setup::resource", key = %kv.0, value = %kv.1), ); } #[derive(Debug)] pub struct ServiceInfoDetector { fallback_service_name: Option>, fallback_service_version: Option>, } impl ResourceDetector for ServiceInfoDetector { fn detect(&self) -> Resource { let service_name = std::env::var("OTEL_SERVICE_NAME") .or_else(|_| std::env::var("SERVICE_NAME")) .or_else(|_| std::env::var("APP_NAME")) .ok() .or_else(|| self.fallback_service_name.clone().map(|v| v.to_string())) .map(|v| KeyValue::new(resource::SERVICE_NAME, v)); let service_version = std::env::var("SERVICE_VERSION") .or_else(|_| std::env::var("APP_VERSION")) .ok() .or_else(|| { self.fallback_service_version .clone() .map(std::borrow::Cow::into_owned) }) .map(|v| KeyValue::new(resource::SERVICE_VERSION, v)); let mut resource = Resource::builder_empty(); if let Some(service_name) = service_name { resource = resource.with_attribute(service_name); } if let Some(service_version) = service_version { resource = resource.with_attribute(service_version); } resource.build() } } ================================================ FILE: init-tracing-opentelemetry/src/stdio.rs ================================================ use crate::Error; use opentelemetry::InstrumentationScope; use opentelemetry::trace::TracerProvider as _; use opentelemetry_sdk::Resource; use opentelemetry_sdk::trace as sdktrace; use opentelemetry_sdk::trace::BatchSpanProcessor; use opentelemetry_sdk::trace::SdkTracerProvider; use opentelemetry_sdk::trace::TracerProviderBuilder; use std::fmt::Debug; use std::io::Write; #[must_use] pub fn identity(v: TracerProviderBuilder) -> TracerProviderBuilder { v } pub fn init_tracer(resource: Resource, transform: F) -> Result where F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder, W: Write + Debug + Send + Sync + 'static, { let exporter = opentelemetry_stdout::SpanExporter::default(); let processor = BatchSpanProcessor::builder(exporter).build(); let mut provider_builder = SdkTracerProvider::builder() .with_span_processor(processor) .with_resource(resource) .with_sampler(sdktrace::Sampler::AlwaysOn); provider_builder = transform(provider_builder); // tracer used in libraries/crates that optionally includes version and schema url let scope = InstrumentationScope::builder(env!("CARGO_PKG_NAME")) .with_version(env!("CARGO_PKG_VERSION")) .with_schema_url("https://opentelemetry.io/schema/1.0.0") .build(); Ok(provider_builder.build().tracer_with_scope(scope)) } #[derive(Debug, Default)] pub struct WriteNoWhere; impl Write for WriteNoWhere { fn write(&mut self, buf: &[u8]) -> std::io::Result { Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } ================================================ FILE: init-tracing-opentelemetry/src/tracing_subscriber_ext.rs ================================================ #![allow(deprecated)] use std::borrow::Cow; use opentelemetry::trace::TracerProvider; #[cfg(feature = "logs")] use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; #[cfg(feature = "logs")] use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider}; #[cfg(feature = "metrics")] use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry_sdk::{ Resource, trace::{SdkTracerProvider, Tracer}, }; use tracing::{Subscriber, level_filters::LevelFilter}; #[cfg(feature = "metrics")] use tracing_opentelemetry::MetricsLayer; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::{Layer, filter::EnvFilter, layer::SubscriberExt, registry::LookupSpan}; use crate::{ Error, config::TracingConfig, init_propagator, //stdio, otlp, otlp::OtelGuard, resource::DetectResource, }; #[must_use] #[deprecated( since = "0.31.0", note = "Use `TracingConfig::default().build_layer()` instead" )] /// # Panics /// Panics if the logger layer cannot be built. pub fn build_logger_text() -> Box + Send + Sync + 'static> where S: Subscriber + for<'a> LookupSpan<'a>, { TracingConfig::default() .build_layer() .expect("Failed to build logger layer") } #[must_use] #[deprecated = "replaced by the configurable build_level_filter_layer(\"\")"] pub fn build_loglevel_filter_layer() -> EnvFilter { build_level_filter_layer("").unwrap_or_default() } /// Read the configuration from (first non empty used, priority top to bottom): /// /// - from parameter `directives` /// - from environment variable `RUST_LOG` /// - from environment variable `OTEL_LOG_LEVEL` /// - default to `Level::INFO` /// /// And add directive to: /// /// - `otel::tracing` should be a level info to emit opentelemetry trace & span /// /// You can customize parameter "directives", by adding: /// /// - `otel::setup=debug` set to debug to log detected resources, configuration read (optional) /// /// see [Directives syntax](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives) pub fn build_level_filter_layer(log_directives: &str) -> Result { let dirs = if log_directives.is_empty() { std::env::var("RUST_LOG") .or_else(|_| std::env::var("OTEL_LOG_LEVEL")) .unwrap_or_else(|_| "info".to_string()) } else { log_directives.to_string() }; let directive_to_allow_otel_trace = "otel::tracing=trace".parse()?; Ok(EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .parse_lossy(dirs) .add_directive(directive_to_allow_otel_trace)) } #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] pub fn regiter_otel_layers( subscriber: S, ) -> Result<(impl Subscriber + for<'span> LookupSpan<'span>, OtelGuard), Error> where S: Subscriber + for<'a> LookupSpan<'a>, { register_otel_layers_with_resource(subscriber, DetectResource::default().build()) } #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] pub fn register_otel_layers_with_resource( subscriber: S, otel_rsrc: Resource, ) -> Result<(impl Subscriber + for<'span> LookupSpan<'span>, OtelGuard), Error> where S: Subscriber + for<'a> LookupSpan<'a>, { #[cfg(feature = "logs")] let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?; #[cfg(feature = "metrics")] let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?; let (trace_layer, tracer_provider) = build_tracer_layer_with_resource(otel_rsrc)?; let subscriber = subscriber.with(trace_layer); #[cfg(feature = "logs")] let subscriber = subscriber.with(logs_layer); #[cfg(feature = "metrics")] let subscriber = subscriber.with(metrics_layer); Ok(( subscriber, OtelGuard { #[cfg(feature = "logs")] logger_provider, #[cfg(feature = "metrics")] meter_provider, tracer_provider, }, )) } /// change (version 0.31): no longer set the global tracer #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] pub fn build_tracer_layer() -> Result<(OpenTelemetryLayer, SdkTracerProvider), Error> where S: Subscriber + for<'span> LookupSpan<'span>, { build_tracer_layer_with_resource( DetectResource::default() //.with_fallback_service_name(env!("CARGO_PKG_NAME")) //.with_fallback_service_version(env!("CARGO_PKG_VERSION")) .build(), ) } #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] pub fn build_tracer_layer_with_resource( otel_rsrc: Resource, ) -> Result<(OpenTelemetryLayer, SdkTracerProvider), Error> where S: Subscriber + for<'span> LookupSpan<'span>, { build_tracer_layer_with_resource_and_name(otel_rsrc, "") } #[cfg(feature = "logs")] pub(crate) fn build_logger_layer_with_resource( otel_rsrc: Resource, ) -> Result< ( OpenTelemetryTracingBridge, SdkLoggerProvider, ), crate::Error, > { let logger_provider = otlp::logs::init_loggerprovider(otel_rsrc, otlp::logs::identity)?; let layer = OpenTelemetryTracingBridge::new(&logger_provider); Ok((layer, logger_provider)) } pub(crate) fn build_tracer_layer_with_resource_and_name( otel_rsrc: Resource, tracer_name: impl Into>, ) -> Result<(OpenTelemetryLayer, SdkTracerProvider), Error> where S: Subscriber + for<'span> LookupSpan<'span>, { let tracer_provider = otlp::traces::init_tracerprovider(otel_rsrc, otlp::traces::identity)?; // to not send trace somewhere, but continue to create and propagate,... // then send them to `init_tracing_opentelemetry::stdio::WriteNoWhere::default()` // or to `std::io::stdout()` to print // // let otel_tracer = stdio::init_tracer( // otel_rsrc, // stdio::identity::, // stdio::WriteNoWhere::default(), // )?; init_propagator()?; let layer = tracing_opentelemetry::layer() .with_error_records_to_exceptions(true) .with_tracer(tracer_provider.tracer(tracer_name)); opentelemetry::global::set_tracer_provider(tracer_provider.clone()); Ok((layer, tracer_provider)) } #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] #[cfg(feature = "metrics")] pub fn build_metrics_layer() -> Result<(MetricsLayer, SdkMeterProvider), Error> where S: Subscriber + for<'span> LookupSpan<'span>, { build_metrics_layer_with_resource(DetectResource::default().build()) } #[deprecated(since = "0.31.0", note = "Use `TracingConfig` instead")] #[cfg(feature = "metrics")] pub fn build_metrics_layer_with_resource( otel_rsrc: Resource, ) -> Result<(MetricsLayer, SdkMeterProvider), Error> where S: Subscriber + for<'a> LookupSpan<'a>, { let meter_provider = otlp::metrics::init_meterprovider(otel_rsrc, otlp::metrics::identity)?; let layer = MetricsLayer::new(meter_provider.clone()); opentelemetry::global::set_meter_provider(meter_provider.clone()); Ok((layer, meter_provider)) } /// Initialize subscribers with default configuration /// /// This is a convenience function that uses production-ready defaults. /// For more control, use `TracingConfig::production().init_subscriber()`. #[deprecated( since = "0.31.0", note = "Use `TracingConfig::production()...` instead" )] pub fn init_subscribers() -> Result { let guard = TracingConfig::production().init_subscriber()?; match guard.otel_guard { Some(otel_guard) => { // For backward compatibility, we leak the default_guard since the caller // only expects an OtelGuard and won't hold onto the DefaultGuard if let Some(default_guard) = guard.default_guard { std::mem::forget(default_guard); } Ok(otel_guard) } None => Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "OpenTelemetry is disabled but OtelGuard was requested", ) .into()), } } /// Initialize subscribers with custom log directives /// /// See [`build_level_filter_layer`] for the syntax of `log_directives`. /// For more control, use `TracingConfig::production().with_log_directives(log_directives).init_subscriber()`. #[deprecated( since = "0.31.0", note = "Use `TracingConfig::production().with_log_directives(log_directives)...` instead" )] pub fn init_subscribers_and_loglevel(log_directives: &str) -> Result { let guard = TracingConfig::production() .with_log_directives(log_directives) .init_subscriber()?; match guard.otel_guard { Some(otel_guard) => { // For backward compatibility, we leak the default_guard since the caller // only expects an OtelGuard and won't hold onto the DefaultGuard if let Some(default_guard) = guard.default_guard { std::mem::forget(default_guard); } Ok(otel_guard) } None => Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "OpenTelemetry is disabled but OtelGuard was requested", ) .into()), } } ================================================ FILE: release-plz.toml ================================================ # [Configuration | Release-plz](https://release-plz.ieni.dev/docs/config) [workspace] features_always_increment_minor = true [changelog] sort_commits = "newest" commit_preprocessors = [ # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, # remove issue numbers from commits # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers ] # regex for parsing and grouping commits # try to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) commit_parsers = [ { message = "^(🔒️|🔐)", group = "Security" }, { body = ".*security", group = "Security" }, { message = "^(fix|🐛|🚑️|👽️)", group = "Fixed" }, { message = "^(test|✅)", group = "Fixed", skip = true }, { message = "^.*: add", group = "Added" }, { message = "^.*: support", group = "Added" }, { message = "^(feat|✨|💥)", group = "Added" }, { message = "^.*: remove", group = "Removed" }, { message = "^.*: delete", group = "Removed" }, { message = "^(style|💄)", group = "Changed" }, { message = "^(doc|✏️|📝)", group = "Changed" }, { message = "^(perf|⚡️)", group = "Changed" }, { message = "^(chore|ci|💚|👷|🚧)", group = "Changed", skip = true }, { message = "^revert", group = "Changed" }, { message = "^(chore\\(deps\\)|⬇️|⬆️|➕|➖)", group = "Changed" }, { message = "^(refactor|🎨|🔥|♻️)", group = "Refactor", skip = true }, { message = "^(chore\\(release\\): prepare for|🔖|🚀)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, ] ================================================ FILE: renovate.json5 ================================================ // https://docs.renovatebot.com/configuration-options/ // https://www.augmentedmind.de/2023/07/30/renovate-bot-cheat-sheet/ { $schema: "https://docs.renovatebot.com/renovate-schema.json", extends: ["config:recommended"], additionalReviewers: ["davidB"], ignoreDeps: [ "rust" ], // to keep mise, rust-toolchain aligned to MSRV packageRules: [ { matchUpdateTypes: ["patch", "pin", "digest"], enabled: false, }, { matchPackageNames: ["helm"], automerge: true, // Force Renovate to not create a PR (but merge its branches directly), to avoid PR-related email spam automergeType: "branch", }, { matchPackageNames: ["/opentelemetry/"], groupName: "opentelemetry", }, { matchPackageNames: ["kube", "k8s-openapi"], groupName: "kubers", }, { matchPackageNames: ["rust"], enabled: false, }, ], } ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "1.91.0" ================================================ FILE: testing-tracing-opentelemetry/Cargo.toml ================================================ [package] name = "testing-tracing-opentelemetry" description = "helpers to help testing app + tracing + opentelemetry." readme = "README.md" keywords = ["tracing", "opentelemetry"] categories = ["development-tools::testing"] homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/testing-tracing-opentelemetry" publish = false edition.workspace = true version = "0.19.0" repository.workspace = true license.workspace = true [dependencies] assert2 = { workspace = true } fake-opentelemetry-collector = { path = "../fake-opentelemetry-collector", version = "0.34" } insta = { workspace = true } opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true } serde_json = "1" tracing = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { version = "0.3", default-features = false, features = [ "env-filter", "fmt", "json", ] } ================================================ FILE: testing-tracing-opentelemetry/src/lib.rs ================================================ use assert2::{assert, check}; use opentelemetry::trace::TracerProvider; use opentelemetry_sdk::propagation::TraceContextPropagator; use serde_json::Value; use std::sync::mpsc::{self, Receiver, SyncSender}; use tracing_subscriber::{ EnvFilter, fmt::{MakeWriter, format::FmtSpan}, util::SubscriberInitExt, }; pub fn assert_trace( name: &str, tracing_events: Vec, otel_spans: Vec, is_trace_id_constant: bool, ) { let trace_id_0 = tracing_events .first() .and_then(|v| v.as_object()) .and_then(|v| v.get("span")) .and_then(|v| v.as_object()) .and_then(|v| v.get("trace_id")) .and_then(|v| v.as_str()) .unwrap_or_default() .to_owned(); // let trace_id_3 = trace_id_0.clone(); let trace_id_1 = trace_id_0.clone(); let trace_id_2 = trace_id_0; insta::assert_yaml_snapshot!(name, tracing_events, { "[].timestamp" => "[timestamp]", "[].fields[\"time.busy\"]" => "[duration]", "[].fields[\"time.idle\"]" => "[duration]", "[].span.trace_id" => insta::dynamic_redaction(move |value, _path| { assert!(let Some(tracing_trace_id) = value.as_str()); check!(trace_id_1 == tracing_trace_id); if is_trace_id_constant { tracing_trace_id.to_string() } else { format!("[trace_id:lg{}]", tracing_trace_id.len()) } }), "[].spans[].trace_id" => insta::dynamic_redaction(move |value, _path| { assert!(let Some(tracing_trace_id) = value.as_str()); check!(trace_id_2 == tracing_trace_id); if is_trace_id_constant { tracing_trace_id.to_string() } else { format!("[trace_id:lg{}]", tracing_trace_id.len()) } }), }); insta::assert_yaml_snapshot!(format!("{}_otel_spans", name), otel_spans, { "[].start_time_unix_nano" => "[timestamp]", "[].end_time_unix_nano" => "[timestamp]", "[].events[].time_unix_nano" => "[timestamp]", "[].trace_id" => insta::dynamic_redaction(move |value, _path| { assert!(let Some(otel_trace_id) = value.as_str()); //FIXME check!(trace_id_3 == otel_trace_id); format!("[trace_id:lg{}]", otel_trace_id.len()) }), "[].span_id" => insta::dynamic_redaction(|value, _path| { assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].parent_span_id" => insta::dynamic_redaction(|value, _path| { assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].links[].trace_id" => insta::dynamic_redaction(|value, _path| { assert!(let Some(otel_trace_id) = value.as_str()); format!("[trace_id:lg{}]", otel_trace_id.len()) }), "[].links[].span_id" => insta::dynamic_redaction(|value, _path| { assert!(let Some(span_id) = value.as_str()); format!("[span_id:lg{}]", span_id.len()) }), "[].attributes.busy_ns" => "ignore", "[].attributes.idle_ns" => "ignore", "[].attributes.trace_id" => "ignore", "[].attributes[\"code.lineno\"]" => "ignore", "[].attributes[\"code.filepath\"]" => "ignore", "[].attributes[\"thread.id\"]" => "ignore", }); } pub struct FakeEnvironment { fake_collector: fake_opentelemetry_collector::FakeCollectorServer, rx: Receiver>, _subsciber_guard: tracing::subscriber::DefaultGuard, tracer_provider: opentelemetry_sdk::trace::SdkTracerProvider, } impl FakeEnvironment { pub async fn setup() -> Self { //use axum::body::HttpBody as _; //use tower::{Service, ServiceExt}; use tracing_subscriber::layer::SubscriberExt; // setup a non Noop OpenTelemetry tracer to have non-empty trace_id let fake_collector = fake_opentelemetry_collector::FakeCollectorServer::start() .await .unwrap(); let tracer_provider = fake_opentelemetry_collector::setup_tracer_provider(&fake_collector).await; //let (tracer, mut req_rx) = fake_opentelemetry_collector::setup_tracer().await; opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("fake")); let (make_writer, rx) = duplex_writer(); let fmt_layer = tracing_subscriber::fmt::layer() .json() .with_writer(make_writer) .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE); let subscriber = tracing_subscriber::registry() .with(EnvFilter::try_new("trace").unwrap()) .with(fmt_layer) .with(otel_layer); let _subsciber_guard = subscriber.set_default(); Self { fake_collector, rx, _subsciber_guard, tracer_provider, } } pub async fn collect_traces( &mut self, ) -> (Vec, Vec) { let _ = self.tracer_provider.force_flush(); let otel_spans = self .fake_collector .exported_spans(1, std::time::Duration::from_millis(100)) .await; // insta::assert_debug_snapshot!(first_span); let tracing_events = std::iter::from_fn(|| { self.rx .recv_timeout(std::time::Duration::from_millis(3)) //.recv() .ok() }) .map(|bytes| serde_json::from_slice::(&bytes).unwrap()) .collect::>(); (tracing_events, otel_spans) } } fn duplex_writer() -> (DuplexWriter, Receiver>) { let (tx, rx) = mpsc::sync_channel(1024); (DuplexWriter { tx }, rx) } #[derive(Clone)] struct DuplexWriter { tx: SyncSender>, } impl<'a> MakeWriter<'a> for DuplexWriter { type Writer = Self; fn make_writer(&'a self) -> Self::Writer { self.clone() } } impl std::io::Write for DuplexWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.tx.send(buf.to_vec()).unwrap(); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__call_with_w3c_trace.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events snapshot_kind: text --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: "/users/{id}" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: "GET /users/{id}" server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__call_with_w3c_trace_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg16]" name: "GET /users/{id}" kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/users/{id}\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_6\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/users/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__empty_http_route_for_nonexisting_route.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /idontexist/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 404 http.route: "" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /idontexist/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__empty_http_route_for_nonexisting_route_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: GET kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"404\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_2\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/idontexist/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__extract_route_from_nested.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events snapshot_kind: text --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /nest/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: "/nest/{nest_id}" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: "GET /nest/{nest_id}" server.address: "" span.type: web url.path: /nest/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__extract_route_from_nested_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: "GET /nest/{nest_id}" kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/nest/{nest_id}\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_9\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/nest/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__filled_http_headers.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events snapshot_kind: text --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: tests spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: "/users/{id}" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: "GET /users/{id}" server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: tests spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__filled_http_headers_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: "GET /users/{id}" kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/users/{id}\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_5\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/users/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"tests\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__filled_http_route_for_existing_route.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events snapshot_kind: text --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: example.com span.type: web url.path: /users/123 url.scheme: http user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: "/users/{id}" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: "GET /users/{id}" server.address: example.com span.type: web url.path: /users/123 url.scheme: http user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__filled_http_route_for_existing_route_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: "GET /users/{id}" kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/users/{id}\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"example.com\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_1\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/users/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"http\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__status_code_on_close_for_error.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /status/500 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 500 http.route: /status/500 name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /status/500 otel.status_code: ERROR server.address: "" span.type: web url.path: /status/500 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__status_code_on_close_for_error_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: GET /status/500 kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"500\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/status/500\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_4\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/status/500\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_ERROR ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__status_code_on_close_for_ok.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events snapshot_kind: text --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: "/users/{id}" name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: "GET /users/{id}" server.address: "" span.type: web url.path: /users/123 url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__status_code_on_close_for_ok_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: "GET /users/{id}" kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/users/{id}\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_3\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/users/123\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__trace_id_in_child_span.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: new level: INFO span: name: my child span spans: - http.request.method: GET http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" target: "axum_tracing_opentelemetry::middleware::trace_extractor::tests" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: INFO span: name: my child span spans: - http.request.method: GET http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" target: "axum_tracing_opentelemetry::middleware::trace_extractor::tests" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__trace_id_in_child_span_for_remote.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: tracing_events --- - fields: message: new level: TRACE span: http.request.method: GET name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" - fields: message: new level: INFO span: name: my child span spans: - http.request.method: GET http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" target: "axum_tracing_opentelemetry::middleware::trace_extractor::tests" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: INFO span: name: my child span spans: - http.request.method: GET http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" target: "axum_tracing_opentelemetry::middleware::trace_extractor::tests" timestamp: "[timestamp]" - fields: message: close time.busy: "[duration]" time.idle: "[duration]" level: TRACE span: http.request.method: GET http.response.status_code: 200 http.route: /with_child_span name: HTTP request network.protocol.version: "1.1" otel.kind: Server otel.name: GET /with_child_span server.address: "" span.type: web url.path: /with_child_span url.scheme: "" user_agent.original: "" spans: [] target: "otel::tracing" timestamp: "[timestamp]" ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__trace_id_in_child_span_for_remote_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg16]" name: my child span kind: SPAN_KIND_INTERNAL start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"axum-tracing-opentelemetry/src/middleware/trace_extractor.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(257)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"axum_tracing_opentelemetry::middleware::trace_extractor::tests\")) })" idle_ns: ignore target: "Some(AnyValue { value: Some(StringValue(\"axum_tracing_opentelemetry::middleware::trace_extractor::tests\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_8\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg16]" name: GET /with_child_span kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/with_child_span\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_8\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/with_child_span\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: testing-tracing-opentelemetry/src/snapshots/testing_tracing_opentelemetry__trace_id_in_child_span_otel_spans.snap ================================================ --- source: testing-tracing-opentelemetry/src/lib.rs expression: otel_spans --- - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg16]" name: my child span kind: SPAN_KIND_INTERNAL start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"axum-tracing-opentelemetry/src/middleware/trace_extractor.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(257)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"axum_tracing_opentelemetry::middleware::trace_extractor::tests\")) })" idle_ns: ignore target: "Some(AnyValue { value: Some(StringValue(\"axum_tracing_opentelemetry::middleware::trace_extractor::tests\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_7\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET - trace_id: "[trace_id:lg32]" span_id: "[span_id:lg16]" trace_state: "" parent_span_id: "[span_id:lg0]" name: GET /with_child_span kind: SPAN_KIND_SERVER start_time_unix_nano: "[timestamp]" end_time_unix_nano: "[timestamp]" attributes: busy_ns: ignore code.file.path: "Some(AnyValue { value: Some(StringValue(\"tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs\")) })" code.line.number: "Some(AnyValue { value: Some(IntValue(15)) })" code.module.name: "Some(AnyValue { value: Some(StringValue(\"tracing_opentelemetry_instrumentation_sdk::http::http_server\")) })" http.request.method: "Some(AnyValue { value: Some(StringValue(\"GET\")) })" http.response.status_code: "Some(AnyValue { value: Some(StringValue(\"200\")) })" http.route: "Some(AnyValue { value: Some(StringValue(\"/with_child_span\")) })" idle_ns: ignore network.protocol.version: "Some(AnyValue { value: Some(StringValue(\"1.1\")) })" server.address: "Some(AnyValue { value: Some(StringValue(\"\")) })" span.type: "Some(AnyValue { value: Some(StringValue(\"web\")) })" target: "Some(AnyValue { value: Some(StringValue(\"otel::tracing\")) })" thread.id: ignore thread.name: "Some(AnyValue { value: Some(StringValue(\"middleware::trace_extractor::tests::check_span_event::case_7\")) })" url.path: "Some(AnyValue { value: Some(StringValue(\"/with_child_span\")) })" url.scheme: "Some(AnyValue { value: Some(StringValue(\"\")) })" user_agent.original: "Some(AnyValue { value: Some(StringValue(\"\")) })" dropped_attributes_count: 0 events: [] dropped_events_count: 0 links: [] dropped_links_count: 0 status: message: "" code: STATUS_CODE_UNSET ================================================ FILE: tonic-tracing-opentelemetry/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.30.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.29.1...tonic-tracing-opentelemetry-v0.30.0) - 2025-09-27 ### Added - [**breaking**] export grpc utils from `http::grpc` module ## [0.29.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.28.1...tonic-tracing-opentelemetry-v0.29.0) - 2025-06-03 ### Added - *(deps)* update opentelemetry 0.30 & tonic 0.13 (#240) ## [0.26.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.26.0...tonic-tracing-opentelemetry-v0.26.1) - 2025-02-26 ### Removed - *(deps)* remove minor constraint when major > 1 ## [0.24.3](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.24.2...tonic-tracing-opentelemetry-v0.24.3) - 2025-01-07 ### Fixed - Implement tower::Service for OtelGrpcService (#201) ## [0.21.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.19.0...tonic-tracing-opentelemetry-v0.21.0) - 2024-08-31 ### Changed - 💄 update deprecated syntax "default_features" in Cargo.toml - ⬆️ upgrade to tonic 0.12 - ⬆️ upgrade to rstest 0.22 ## [0.18.2](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tonic-tracing-opentelemetry-v0.18.1...tonic-tracing-opentelemetry-v0.18.2) - 2024-04-24 ### Added - ✨ allow to create span for opentelemetry at level `info` with feature flag `tracing_level_info` ================================================ FILE: tonic-tracing-opentelemetry/Cargo.toml ================================================ [package] name = "tonic-tracing-opentelemetry" description = "Middlewares and tools to integrate tonic + tracing + opentelemetry." readme = "README.md" keywords = ["tonic", "tracing", "opentelemetry"] categories = [ "development-tools::debugging", "development-tools::profiling", "web-programming", ] homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/tonic-tracing-opentelemetry" edition.workspace = true version = "0.32.2" repository.workspace = true license.workspace = true [dependencies] futures-core = "0.3" futures-util = { version = "0.3", default-features = false, features = [] } http = { workspace = true } http-body = "1" hyper = { workspace = true } opentelemetry = { workspace = true } pin-project-lite = "0.2" tonic = { workspace = true, default-features = false } tower = { workspace = true } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-opentelemetry-instrumentation-sdk = { path = "../tracing-opentelemetry-instrumentation-sdk", features = [ "http", ], version = "0.32" } [dev-dependencies] axum = { workspace = true } testing-tracing-opentelemetry = { path = "../testing-tracing-opentelemetry" } fake-opentelemetry-collector = { path = "../fake-opentelemetry-collector" } assert2 = { workspace = true } insta = { workspace = true } opentelemetry-otlp = { workspace = true, features = [ "http-proto", "reqwest-client", "reqwest-rustls", ] } opentelemetry-proto = { workspace = true, features = ["gen-tonic"] } rstest = { workspace = true } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { workspace = true, features = ["full"] } tracing-subscriber = { version = "0.3", default-features = false, features = [ "env-filter", "fmt", "json", ] } tokio-stream = { workspace = true, features = ["net"] } # need tokio runtime to run smoke tests. opentelemetry_sdk = { workspace = true, features = [ "trace", "rt-tokio", "testing", ] } [features] default = [] # to use level `info` instead of `trace` to create otel span tracing_level_info = [ "tracing-opentelemetry-instrumentation-sdk/tracing_level_info", ] ================================================ FILE: tonic-tracing-opentelemetry/README.md ================================================ # tonic-tracing-opentelemetry [![crates license](https://img.shields.io/crates/l/tonic-tracing-opentelemetry.svg)](http://creativecommons.org/publicdomain/zero/1.0/) [![crate version](https://img.shields.io/crates/v/tonic-tracing-opentelemetry.svg)](https://crates.io/crates/tonic-tracing-opentelemetry) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) Middlewares and tools to integrate tonic + tracing + opentelemetry for client and server. > Really early, missing lot of features, help is welcomed. - Read OpenTelemetry header from the incoming requests - Start a new trace if no trace is found in the incoming request - Trace is attached into tracing's span For examples, you can look at the [examples](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/examples/) folder. Extract of `client.rs`: ```txt let channel = Channel::from_static("http://127.0.0.1:50051") .connect() .await?; //Devskim: ignore DS137138 let channel = ServiceBuilder::new() .layer(OtelGrpcLayer::default()) .service(channel); let mut client = GreeterClient::new(channel); //... opentelemetry::global::shutdown_tracer_provider(); ``` Extract of `server.rs`: ```txt Server::builder() // create trace for every request including health_service .layer(server::OtelGrpcLayer::default().filter(filters::reject_healthcheck)) .add_service(health_service) .add_service(reflection_service) //.add_service(GreeterServer::new(greeter)) .add_service(GreeterServer::new(greeter)) .serve_with_shutdown(addr, shutdown_signal()) .await?; ``` ## TODO - add test - add documentation - add examples - validate with [[opentelemetry-specification/rpc.md at main · open-telemetry/opentelemetry-specification · GitHub](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#grpc)] ## Changelog - History [CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md) ================================================ FILE: tonic-tracing-opentelemetry/src/lib.rs ================================================ //#![warn(missing_docs)] #![forbid(unsafe_code)] #![warn(clippy::perf)] #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![doc = include_str!("../README.md")] pub mod middleware; ================================================ FILE: tonic-tracing-opentelemetry/src/middleware/client.rs ================================================ //! code based on [tonic/examples/src/tower/client.rs at master · hyperium/tonic · GitHub](https://github.com/hyperium/tonic/blob/master/examples/src/tower/client.rs) use http::{Request, Response}; use pin_project_lite::pin_project; use std::{ error::Error, future::Future, pin::Pin, task::{Context, Poll}, }; use tonic::client::GrpcService; use tower::{Layer, Service}; use tracing::Span; use tracing_opentelemetry_instrumentation_sdk::{find_context_from_tracing, http as otel_http}; /// layer for grpc (tonic client): /// /// - propagate `OpenTelemetry` context (`trace_id`,...) to server /// - create a Span for `OpenTelemetry` (and tracing) on call /// /// `OpenTelemetry` context are extracted frim tracing's span. #[derive(Default, Debug, Clone)] pub struct OtelGrpcLayer; impl Layer for OtelGrpcLayer { /// The wrapped service type Service = OtelGrpcService; fn layer(&self, inner: S) -> Self::Service { OtelGrpcService { inner } } } #[derive(Debug, Clone)] pub struct OtelGrpcService { inner: S, } impl Service> for OtelGrpcService where S: GrpcService + Clone + Send + 'static, S::Future: Send + 'static, S::Error: Error + 'static, B: Send + 'static, // B2: tonic::codegen::Body, B2: http_body::Body, { type Response = Response; type Error = S::Error; type Future = ResponseFuture; // #[allow(clippy::type_complexity)] // type Future = // futures::future::BoxFuture<'static, Result, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) //.map_err(|e| e.into()) } fn call(&mut self, req: Request) -> Self::Future { // This is necessary because tonic internally uses `tower::buffer::Buffer`. // See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149 // for details on why this is necessary // let clone = self.inner.clone(); // let mut inner = std::mem::replace(&mut self.inner, clone); let mut req = req; let span = otel_http::grpc_client::make_span_from_request(&req); otel_http::inject_context(&find_context_from_tracing(&span), req.headers_mut()); let future = { let _enter = span.enter(); self.inner.call(req) }; ResponseFuture { inner: future, span, } } } pin_project! { /// Response future for [`Trace`]. /// /// [`Trace`]: super::Trace pub struct ResponseFuture { #[pin] pub(crate) inner: F, pub(crate) span: Span, // pub(crate) start: Instant, } } impl Future for ResponseFuture where Fut: Future, E>>, E: std::error::Error + 'static, { type Output = Result, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let _guard = this.span.enter(); let result = futures_util::ready!(this.inner.poll(cx)); otel_http::grpc::update_span_from_response_or_error(this.span, &result); Poll::Ready(result) } } ================================================ FILE: tonic-tracing-opentelemetry/src/middleware/filters.rs ================================================ #[must_use] pub fn reject_healthcheck(path: &str) -> bool { !path.contains("grpc.health.") //"grpc.health.v1.Health" } ================================================ FILE: tonic-tracing-opentelemetry/src/middleware/mod.rs ================================================ pub mod client; pub mod filters; pub mod server; ================================================ FILE: tonic-tracing-opentelemetry/src/middleware/server.rs ================================================ //! code based on [tonic/examples/src/tower/client.rs at master · hyperium/tonic · GitHub](https://github.com/hyperium/tonic/blob/master/examples/src/tower/client.rs) use http::{Request, Response}; use pin_project_lite::pin_project; use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use tower::{Layer, Service}; use tracing::Span; use tracing_opentelemetry_instrumentation_sdk::http as otel_http; pub type Filter = fn(&str) -> bool; /// layer for grpc (tonic client): /// /// - propagate `OpenTelemetry` context (`trace_id`, ...) to server /// - create a Span for `OpenTelemetry` (and tracing) on call /// /// `OpenTelemetry` context are extracted frim tracing's span. #[derive(Default, Debug, Clone)] pub struct OtelGrpcLayer { filter: Option, } // add a builder like api impl OtelGrpcLayer { #[must_use] pub fn filter(self, filter: Filter) -> Self { OtelGrpcLayer { filter: Some(filter), } } } impl Layer for OtelGrpcLayer { /// The wrapped service type Service = OtelGrpcService; fn layer(&self, inner: S) -> Self::Service { OtelGrpcService { inner, filter: self.filter, } } } #[derive(Debug, Clone)] pub struct OtelGrpcService { inner: S, filter: Option, } impl Service> for OtelGrpcService where S: Service, Response = Response> + Clone + Send + 'static, //S::Future: Send + 'static, S::Error: std::error::Error, B: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = ResponseFuture; // #[allow(clippy::type_complexity)] // type Future = Pin> + Send>>; //type Future = futures_core::future::BoxFuture<'static, Result>; //type Future = Pin>; // type Future = S::Future; //type Future = Inspect>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { use tracing_opentelemetry::OpenTelemetrySpanExt; // This is necessary because tonic internally uses `tower::buffer::Buffer`. // See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149 // for details on why this is necessary // let clone = self.inner.clone(); // let mut inner = std::mem::replace(&mut self.inner, clone); let req = req; let span = if self.filter.is_none_or(|f| f(req.uri().path())) { let span = otel_http::grpc_server::make_span_from_request(&req); if let Err(error) = span.set_parent(otel_http::extract_context(req.headers())) { tracing::warn!(?error, "can not set parent trace_id to span"); } span } else { tracing::Span::none() }; let future = { let _enter = span.enter(); self.inner.call(req) }; ResponseFuture { inner: future, span, } } } pin_project! { /// Response future for [`Trace`]. /// /// [`Trace`]: super::Trace pub struct ResponseFuture { #[pin] pub(crate) inner: F, pub(crate) span: Span, // pub(crate) start: Instant, } } impl Future for ResponseFuture where Fut: Future, Error>>, // Require that the inner service's error can be converted into a `BoxError`. //Error: Into, Error: std::error::Error, { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let _guard = this.span.enter(); let result = futures_util::ready!(this.inner.poll(cx)); otel_http::grpc::update_span_from_response_or_error(this.span, &result); Poll::Ready(result) } } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.30.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.29.1...tracing-opentelemetry-instrumentation-sdk-v0.30.0) - 2025-08-25 ### Added - *(axum)* optional extraction of `client.address` (former `client_ip`) from http headers or socket's info # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.32.4](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.32.3...tracing-opentelemetry-instrumentation-sdk-v0.32.4) - 2026-03-15 ### Fixed - *(deps)* MSRV bump rust to 1.91 ## [0.32.4](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.32.3...tracing-opentelemetry-instrumentation-sdk-v0.32.4) - 2026-03-15 ### Fixed - *(deps)* MSRV bump rust to 1.91 ## [0.32.2](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.32.1...tracing-opentelemetry-instrumentation-sdk-v0.32.2) - 2025-11-13 ### Added - add in features to docs.rs rendered content. ([#287](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/pull/287)) ## [0.32.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.32.0...tracing-opentelemetry-instrumentation-sdk-v0.32.1) - 2025-10-14 ### Wip - use `opentelemetry-semantic-conventions` instead of `static &str` ## [0.31.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.30.0...tracing-opentelemetry-instrumentation-sdk-v0.31.0) - 2025-09-27 ### Added - [**breaking**] export grpc utils from `http::grpc` module ## [0.29.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.28.1...tracing-opentelemetry-instrumentation-sdk-v0.29.0) - 2025-06-03 ### Added - *(deps)* update opentelemetry 0.30 & tonic 0.13 (#240) ## [0.24.0](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.19.0...tracing-opentelemetry-instrumentation-sdk-v0.24.0) - 2024-08-31 ### Changed - ⬆️ upgrade to tonic 0.12 - ⬆️ upgrade to rstest 0.22 ## [0.18.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.18.0...tracing-opentelemetry-instrumentation-sdk-v0.18.1) - 2024-04-24 ### Added - ✨ allow to create span for opentelemetry at level `info` with feature flag `tracing_level_info` ## [0.17.1](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/compare/tracing-opentelemetry-instrumentation-sdk-v0.17.0...tracing-opentelemetry-instrumentation-sdk-v0.17.1) - 2024-02-24 ### Other - 👷 tune release-plz ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/Cargo.toml ================================================ [package] name = "tracing-opentelemetry-instrumentation-sdk" description = "A set of helpers to build OpenTelemetry instrumentation based on `tracing` crate." readme = "README.md" keywords = ["tracing", "opentelemetry"] categories = [ "development-tools::debugging", "development-tools::profiling", "web-programming", ] homepage = "https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/tree/main/tracing-opentelemetry-instrumentation-sdk" edition.workspace = true version = "0.32.5" repository.workspace = true license.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docs_rs"] [dependencies] http = { workspace = true, optional = true } opentelemetry = { workspace = true } opentelemetry-semantic-conventions = { workspace = true, features = [ "semconv_experimental", ] } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } [dev-dependencies] assert2 = { workspace = true } rstest = { workspace = true } [features] default = [] http = ["dep:http"] # to use level `info` instead of `trace` to create otel span tracing_level_info = [] [lints] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docs_rs)'] } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/README.md ================================================ # tracing-opentelemetry-instrumentation-sdk Provide a set of helpers to build [OpenTelemetry] instrumentation based on [`tracing`] crate, and following the [OpenTelemetry Trace Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/v1.22.0/specification/trace/semantic_conventions). PS: Contributions are welcome (bug report, improvements, features, ...) Instrumentation on the caller side of a call is composed of steps: - start a span with all the attributes (some set to `Empty`) - inject into the call (via header) the propagation data (if supported) - do the call - update attributes of the span with response (status,...) Instrumentation on the callee side of a call is composed of steps: - extract info propagated info (from header) (if supported) an create an OpenTelemetry Context - start a span with all the attributes (some set to `Empty`) - attach the context as parent on the span - do the processing - update attributes of the span with response (status,...) The crates provide helper (or inspiration) to extract/inject context info, start & update span and retrieve context or `trace_id` during processing (eg to inject `trace_id` into log, error message,...). ```rust let trace_id = tracing_opentelemetry_instrumentation_sdk::find_current_trace_id(); //json!({ "error" : "xxxxxx", "trace_id": trace_id}) ``` The helpers could be used as is or into middleware build on it (eg: [`axum-tracing-opentelemetry`], [`tonic-tracing-opentelemetry`] are middlewares build on top of the helpers provide for `http` (feature & crate)) ## Notes - [`tracing-opentelemetry`] extends [`tracing`] to interoperate with [OpenTelemetry]. But with some constraints: - Creation of the OpenTelemetry's span is done when the tracing span is closed. So do not try to interact with OpenTelemetry Span (or `SpanBuilder`) from inside the tracing span. - The OpenTelemetry parent `Context` (and `trace_id`) is created on `NEW` span or inherited from parent span. The parent context can be overwritten after creation, but until then the `trace_id` is the one from `NEW`, So tracing's log could report none or not-yet set `trace_id` on event `NEW` and the following until update. - To define kind, name,... of OpenTelemetry's span from tracing's span used special record's name: `otel.name`, `otel.kind`, ... - Record in a [`tracing`]'s Span should be defined at creation time. So some field are created with value `tracing::field::Empty` to then being updated. - Create trace with target `otel::tracing` (and level `trace`), to have a common way to enable / to disable ## Instrumentations Tips Until every crates are instrumented Use `tracing::instrumented` (no propagation & no update on response) ```txt // basic handmade span far to be compliant with //[opentelemetry-specification/.../database.md](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.22.0/specification/trace/semantic_conventions/database.md) fn make_otel_span(db_operation: &str) -> tracing::Span { // NO parsing of statement to extract information, not recommended by Specification and time-consuming // warning: providing the statement could leek information tracing_opentelemetry_instrumentation_sdk::otel_trace_span!( "DB request", db.system = "postgresql", // db.statement = stmt, db.operation = db_operation, otel.name = db_operation, // should be ., otel.kind = "CLIENT", otel.status_code = tracing::field::Empty, ) } // Insert or update sqlx::query!( "INSERT INTO ...", id, sub_key, result, ) .execute(&*self.pool) .instrument(make_otel_span("INSERT")) .await .map_err(...)?; ``` ## Related crates - [`init-tracing-opentelemetry`] to initialize [`tracing`] & [OpenTelemetry] - [`axum-tracing-opentelemetry`] middlewares for axum based on [`tracing-opentelemetry-instrumentation-sdk`] - [`tonic-tracing-opentelemetry`] middlewares for tonic based on [`tracing-opentelemetry-instrumentation-sdk`] [`tracing-opentelemetry`]: https://crates.io/crates/tracing-opentelemetry [OpenTelemetry]: https://crates.io/crates/opentelemetry [`tracing`]: https://crates.io/crates/tracing [`axum-tracing-opentelemetry`]: https://crates.io/crates/axum-tracing-opentelemetry [`init-tracing-opentelemetry`]: https://crates.io/crates/init-tracing-opentelemetry [`tonic-tracing-opentelemetry`]: https://crates.io/crates/tonic-tracing-opentelemetry [`tracing-opentelemetry-instrumentation-sdk`]: https://crates.io/crates/tracing-opentelemetry-instrumentation-sdk ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/grpc.rs ================================================ use http::HeaderMap; use opentelemetry_semantic_conventions::attribute::{ EXCEPTION_MESSAGE, OTEL_STATUS_CODE, RPC_GRPC_STATUS_CODE, }; /// [`gRPC` status codes](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc) /// copied from tonic #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[allow(dead_code)] pub enum GrpcCode { /// The operation completed successfully. Ok = 0, /// The operation was cancelled. Cancelled = 1, /// Unknown error. Unknown = 2, /// Client specified an invalid argument. InvalidArgument = 3, /// Deadline expired before operation could complete. DeadlineExceeded = 4, /// Some requested entity was not found. NotFound = 5, /// Some entity that we attempted to create already exists. AlreadyExists = 6, /// The caller does not have permission to execute the specified operation. PermissionDenied = 7, /// Some resource has been exhausted. ResourceExhausted = 8, /// The system is not in a state required for the operation's execution. FailedPrecondition = 9, /// The operation was aborted. Aborted = 10, /// Operation was attempted past the valid range. OutOfRange = 11, /// Operation is not implemented or not supported. Unimplemented = 12, /// Internal error. Internal = 13, /// The service is currently unavailable. Unavailable = 14, /// Unrecoverable data loss or corruption. DataLoss = 15, /// The request does not have valid authentication credentials Unauthenticated = 16, } /// If "grpc-status" can not be extracted from http response, the status "0" (Ok) is defined //TODO create similar but with tonic::Response ? and use of [Status in tonic](https://docs.rs/tonic/latest/tonic/struct.Status.html) (more complete) pub fn update_span_from_response( span: &tracing::Span, response: &http::Response, is_spankind_server: bool, ) { let status = status_from_http_header(response.headers()) .or_else(|| status_from_http_status(response.status())) .unwrap_or(GrpcCode::Ok as u16); span.record(RPC_GRPC_STATUS_CODE, status); if status_is_error(status, is_spankind_server) { span.record(OTEL_STATUS_CODE, "ERROR"); } else { span.record(OTEL_STATUS_CODE, "OK"); } } /// based on [Status in tonic](https://docs.rs/tonic/latest/tonic/struct.Status.html#method.from_header_map) fn status_from_http_header(headers: &HeaderMap) -> Option { headers .get("grpc-status") .and_then(|v| v.to_str().ok()) .and_then(|v| v.parse::().ok()) } fn status_from_http_status(status_code: http::StatusCode) -> Option { match status_code { // Borrowed from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md http::StatusCode::BAD_REQUEST => Some(GrpcCode::Internal as u16), http::StatusCode::UNAUTHORIZED => Some(GrpcCode::Unauthenticated as u16), http::StatusCode::FORBIDDEN => Some(GrpcCode::PermissionDenied as u16), http::StatusCode::NOT_FOUND => Some(GrpcCode::Unimplemented as u16), http::StatusCode::TOO_MANY_REQUESTS | http::StatusCode::BAD_GATEWAY | http::StatusCode::SERVICE_UNAVAILABLE | http::StatusCode::GATEWAY_TIMEOUT => Some(GrpcCode::Unavailable as u16), // We got a 200 but no trailers, we can infer that this request is finished. // // This can happen when a streaming response sends two Status but // gRPC requires that we end the stream after the first status. // // https://github.com/hyperium/tonic/issues/681 http::StatusCode::OK => None, _ => Some(GrpcCode::Unknown as u16), } } #[inline] #[must_use] /// see [Semantic Conventions for gRPC | OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/rpc/grpc/) /// see [GRPC Core: Status codes and their use in gRPC](https://grpc.github.io/grpc/core/md_doc_statuscodes.html) pub fn status_is_error(status: u16, is_spankind_server: bool) -> bool { if is_spankind_server { status == 2 || status == 4 || status == 12 || status == 13 || status == 14 || status == 15 } else { status != 0 } } fn update_span_from_error(span: &tracing::Span, error: &E) where E: std::error::Error, { span.record(OTEL_STATUS_CODE, "ERROR"); span.record(RPC_GRPC_STATUS_CODE, 2); span.record(EXCEPTION_MESSAGE, error.to_string()); error .source() .map(|s| span.record(EXCEPTION_MESSAGE, s.to_string())); } pub fn update_span_from_response_or_error( span: &tracing::Span, response: &Result, E>, ) where E: std::error::Error, { match response { Ok(response) => { update_span_from_response(span, response, true); } Err(err) => { update_span_from_error(span, err); } } } // [opentelemetry-specification/.../rpc.md](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md) //TODO create similar but with tonic::Request ? #[allow(clippy::needless_pass_by_value)] pub(crate) fn make_span_from_request( req: &http::Request, kind: opentelemetry::trace::SpanKind, ) -> tracing::Span { use crate::http::{extract_service_method, http_host, user_agent}; use crate::otel_trace_span; use tracing::field::Empty; let (service, method) = extract_service_method(req.uri()); otel_trace_span!( "GRPC request", http.user_agent = %user_agent(req), otel.name = format!("{service}/{method}"), otel.kind = ?kind, otel.status_code = Empty, rpc.system ="grpc", rpc.service = %service, rpc.method = %method, rpc.grpc.status_code = Empty, // to set on response server.address = %http_host(req), exception.message = Empty, // to set on response exception.details = Empty, // to set on response ) } // if let Some(host_name) = SYSTEM.host_name() { // attributes.push(NET_HOST_NAME.string(host_name)); // } #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[rstest] #[case(0)] #[case(16)] #[case(-1)] fn test_status_from_http_header(#[case] input: i32) { let mut headers = http::HeaderMap::new(); headers.insert("grpc-status", input.to_string().parse().unwrap()); if input > -1 { assert_eq!( status_from_http_header(&headers), Some(u16::try_from(input).unwrap()) ); } else { assert_eq!(status_from_http_header(&headers), None); } } } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/grpc_client.rs ================================================ use super::grpc; pub fn make_span_from_request(req: &http::Request) -> tracing::Span { grpc::make_span_from_request(req, opentelemetry::trace::SpanKind::Client) } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/grpc_server.rs ================================================ use super::grpc; pub fn make_span_from_request(req: &http::Request) -> tracing::Span { grpc::make_span_from_request(req, opentelemetry::trace::SpanKind::Server) } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/http_server.rs ================================================ use std::error::Error; use crate::http::{http_flavor, http_host, url_scheme, user_agent}; use crate::otel_trace_span; use crate::span_type::SpanType; use opentelemetry_semantic_conventions::attribute::OTEL_STATUS_CODE; use opentelemetry_semantic_conventions::trace::{EXCEPTION_MESSAGE, HTTP_RESPONSE_STATUS_CODE}; use tracing::field::Empty; pub fn make_span_from_request(req: &http::Request) -> tracing::Span { // [semantic-conventions/.../http-spans.md](https://github.com/open-telemetry/semantic-conventions/blob/v1.25.0/docs/http/http-spans.md) // [semantic-conventions/.../general/attributes.md](https://github.com/open-telemetry/semantic-conventions/blob/v1.25.0/docs/general/attributes.md) // Can not use const or opentelemetry_semantic_conventions::trace::* for name of records let http_method = req.method(); otel_trace_span!( "HTTP request", http.request.method = %http_method, http.route = Empty, // to set by router of "webframework" after network.protocol.version = %http_flavor(req.version()), server.address = http_host(req), // server.port = req.uri().port(), http.client.address = Empty, //%$request.connection_info().realip_remote_addr().unwrap_or(""), user_agent.original = user_agent(req), http.response.status_code = Empty, // to set on response url.path = req.uri().path(), url.query = req.uri().query(), url.scheme = url_scheme(req.uri()), otel.name = %http_method, // to set by router of "webframework" after otel.kind = ?opentelemetry::trace::SpanKind::Server, otel.status_code = Empty, // to set on response trace_id = Empty, // to set on response request_id = Empty, // to set exception.message = Empty, // to set on response "span.type" = %SpanType::Web, // non-official open-telemetry key, only supported by Datadog ) } pub fn update_span_from_response(span: &tracing::Span, response: &http::Response) { let status = response.status(); span.record(HTTP_RESPONSE_STATUS_CODE, status.as_u16()); if status.is_server_error() { span.record(OTEL_STATUS_CODE, "ERROR"); // see [http-spans.md#status](https://github.com/open-telemetry/semantic-conventions/blob/v1.25.0/docs/http/http-spans.md#status) // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, // unless there was another error (e.g., network error receiving the response body; // or 3xx codes with max redirects exceeded), in which case status MUST be set to Error. // } else { // span.record(OTEL_STATUS_CODE, "OK"); } } pub fn update_span_from_error(span: &tracing::Span, error: &E) where E: Error, { span.record(OTEL_STATUS_CODE, "ERROR"); //span.record(HTTP_RESPONSE_STATUS_CODE, 500); span.record(EXCEPTION_MESSAGE, error.to_string()); error .source() .map(|s| span.record(EXCEPTION_MESSAGE, s.to_string())); } pub fn update_span_from_response_or_error( span: &tracing::Span, response: &Result, E>, ) where E: Error, { match response { Ok(response) => { update_span_from_response(span, response); } Err(err) => { update_span_from_error(span, err); } } } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/mod.rs ================================================ pub mod grpc; pub mod grpc_client; pub mod grpc_server; pub mod http_server; mod opentelemetry_http; mod tools; pub use tools::*; ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/opentelemetry_http.rs ================================================ use opentelemetry::propagation::{Extractor, Injector}; // copy from crate opentelemetry-http (to not be dependants of on 3rd: http, ...) pub struct HeaderInjector<'a>(pub &'a mut http::HeaderMap); impl Injector for HeaderInjector<'_> { /// Set a key and value in the `HeaderMap`. Does nothing if the key or value are not valid inputs. fn set(&mut self, key: &str, value: String) { if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) && let Ok(val) = http::header::HeaderValue::from_str(&value) { self.0.insert(name, val); } } } pub struct HeaderExtractor<'a>(pub &'a http::HeaderMap); impl Extractor for HeaderExtractor<'_> { /// Get a value for a key from the `HeaderMap`. If the value is not valid ASCII, returns None. fn get(&self, key: &str) -> Option<&str> { self.0.get(key).and_then(|value| value.to_str().ok()) } /// Collect all the keys from the `HeaderMap`. fn keys(&self) -> Vec<&str> { self.0 .keys() .map(http::HeaderName::as_str) .collect::>() } } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/http/tools.rs ================================================ use std::borrow::Cow; use http::{HeaderMap, Uri, Version}; use opentelemetry::Context; use super::opentelemetry_http::{HeaderExtractor, HeaderInjector}; pub fn inject_context(context: &Context, headers: &mut http::HeaderMap) { let mut injector = HeaderInjector(headers); opentelemetry::global::get_text_map_propagator(|propagator| { propagator.inject_context(context, &mut injector); }); } // If remote request has no span data the propagator defaults to an unsampled context #[must_use] pub fn extract_context(headers: &http::HeaderMap) -> Context { let extractor = HeaderExtractor(headers); opentelemetry::global::get_text_map_propagator(|propagator| propagator.extract(&extractor)) } pub fn extract_service_method(uri: &Uri) -> (&str, &str) { let path = uri.path(); let mut parts = path.split('/').filter(|x| !x.is_empty()); let service = parts.next().unwrap_or_default(); let method = parts.next().unwrap_or_default(); (service, method) } #[must_use] // From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) // > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed. // > This means that, given well-behaved client and proxies, // > the rightmost IP address is the IP address of the most recent proxy and // > the leftmost IP address is the IP address of the originating client. pub fn extract_client_ip_from_headers(headers: &HeaderMap) -> Option<&str> { extract_client_ip_from_forwarded(headers) .or_else(|| extract_client_ip_from_x_forwarded_for(headers)) } #[must_use] // From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) // > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed. // > This means that, given well-behaved client and proxies, // > the rightmost IP address is the IP address of the most recent proxy and // > the leftmost IP address is the IP address of the originating client. fn extract_client_ip_from_x_forwarded_for(headers: &HeaderMap) -> Option<&str> { let value = headers.get("x-forwarded-for")?; let value = value.to_str().ok()?; let mut ips = value.split(','); Some(ips.next()?.trim()) } #[must_use] // see [Forwarded header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded) fn extract_client_ip_from_forwarded(headers: &HeaderMap) -> Option<&str> { let value = headers.get("forwarded")?; let value = value.to_str().ok()?; value .split(';') .flat_map(|directive| directive.split(',')) // select the left/first "for" key .find_map(|directive| directive.trim().strip_prefix("for=")) // ipv6 are enclosed into `["..."]` // string are enclosed into `"..."` .map(|directive| { directive .trim_start_matches('[') .trim_end_matches(']') .trim_matches('"') .trim() }) } #[inline] pub fn http_target(uri: &Uri) -> &str { uri.path_and_query() .map_or("", http::uri::PathAndQuery::as_str) } #[inline] #[must_use] pub fn http_flavor(version: Version) -> Cow<'static, str> { match version { Version::HTTP_09 => "0.9".into(), Version::HTTP_10 => "1.0".into(), Version::HTTP_11 => "1.1".into(), Version::HTTP_2 => "2.0".into(), Version::HTTP_3 => "3.0".into(), other => format!("{other:?}").into(), } } #[inline] pub fn url_scheme(uri: &Uri) -> &str { uri.scheme_str().unwrap_or_default() } #[inline] pub fn user_agent(req: &http::Request) -> &str { req.headers() .get(http::header::USER_AGENT) .map_or("", |h| h.to_str().unwrap_or("")) } #[inline] pub fn http_host(req: &http::Request) -> &str { req.headers() .get(http::header::HOST) .map_or(req.uri().host(), |h| h.to_str().ok()) .unwrap_or("") } #[cfg(test)] mod tests { use super::*; use assert2::assert; use rstest::rstest; #[rstest] // #[case("", "", "")] #[case("/", "", "")] #[case("//", "", "")] #[case("/grpc.health.v1.Health/Check", "grpc.health.v1.Health", "Check")] fn test_extract_service_method( #[case] path: &str, #[case] service: &str, #[case] method: &str, ) { assert!(extract_service_method(&path.parse::().unwrap()) == (service, method)); } #[rstest] #[case("http://example.org/hello/world", "http")] // Devskim: ignore DS137138 #[case("https://example.org/hello/world", "https")] #[case("foo://example.org/hello/world", "foo")] fn test_extract_url_scheme(#[case] input: &str, #[case] expected: &str) { let uri: Uri = input.parse().unwrap(); assert!(url_scheme(&uri) == expected); } #[rstest] #[case("", "")] #[case( "2001:db8:85a3:8d3:1319:8a2e:370:7348", "2001:db8:85a3:8d3:1319:8a2e:370:7348" )] #[case("203.0.113.195", "203.0.113.195")] #[case("203.0.113.195,10.10.10.10", "203.0.113.195")] #[case("203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348", "203.0.113.195")] fn test_extract_client_ip_from_x_forwarded_for(#[case] input: &str, #[case] expected: &str) { let mut headers = HeaderMap::new(); if !input.is_empty() { headers.insert("X-Forwarded-For", input.parse().unwrap()); } let expected = if expected.is_empty() { None } else { Some(expected) }; assert!(extract_client_ip_from_x_forwarded_for(&headers) == expected); } #[rstest] #[case("", "")] #[case( "for=[\"2001:db8:85a3:8d3:1319:8a2e:370:7348\"]", "2001:db8:85a3:8d3:1319:8a2e:370:7348" )] #[case("for=203.0.113.195", "203.0.113.195")] #[case("for=203.0.113.195, for=10.10.10.10", "203.0.113.195")] #[case( "for=203.0.113.195, for=[\"2001:db8:85a3:8d3:1319:8a2e:370:7348\"]", "203.0.113.195" )] #[case("for=\"_mdn\"", "_mdn")] #[case("for=\"secret\"", "secret")] #[case("for=203.0.113.195;proto=http;by=203.0.113.43", "203.0.113.195")] #[case("proto=http;by=203.0.113.43", "")] fn test_extract_client_ip_from_forwarded(#[case] input: &str, #[case] expected: &str) { let mut headers = HeaderMap::new(); if !input.is_empty() { headers.insert("Forwarded", input.parse().unwrap()); } let expected = if expected.is_empty() { None } else { Some(expected) }; assert!(extract_client_ip_from_forwarded(&headers) == expected); } } ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/lib.rs ================================================ //#![warn(missing_docs)] #![forbid(unsafe_code)] #![warn(clippy::perf)] #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![doc = include_str!("../README.md")] #![cfg_attr(docs_rs, feature(doc_cfg))] #[cfg(feature = "http")] #[cfg_attr(docs_rs, doc(cfg(feature = "http")))] pub mod http; mod span_type; use opentelemetry::Context; /// tracing's target used by instrumentation library to create span pub const TRACING_TARGET: &str = "otel::tracing"; #[cfg(not(feature = "tracing_level_info"))] pub const TRACING_LEVEL: tracing::Level = tracing::Level::TRACE; #[cfg(feature = "tracing_level_info")] pub const TRACING_LEVEL: tracing::Level = tracing::Level::INFO; // const SPAN_NAME_FIELD: &str = "otel.name"; // const SPAN_KIND_FIELD: &str = "otel.kind"; // const SPAN_STATUS_CODE_FIELD: &str = "otel.status_code"; // const SPAN_STATUS_MESSAGE_FIELD: &str = "otel.status_message"; // const FIELD_EXCEPTION_MESSAGE: &str = "exception.message"; // const FIELD_EXCEPTION_STACKTRACE: &str = "exception.stacktrace"; // const HTTP_TARGET: &str = opentelemetry_semantic_conventions::trace::HTTP_TARGET.as_str(); /// Constructs a span for the target `TRACING_TARGET` with the level `TRACING_LEVEL`. /// /// [Fields] and [attributes] are set using the same syntax as the [`tracing::span!`] /// macro. //TODO find a way to use opentelemetry_semantic_conventions::attribute::* as part of the field #[macro_export] macro_rules! otel_trace_span { (parent: $parent:expr, $name:expr, $($field:tt)*) => { tracing::span!( target: $crate::TRACING_TARGET, parent: $parent, $crate::TRACING_LEVEL, $name, $($field)* ) }; (parent: $parent:expr, $name:expr) => { $crate::otel_trace_span!(parent: $parent, $name,) }; ($name:expr, $($field:tt)*) => { tracing::span!( target: $crate::TRACING_TARGET, $crate::TRACING_LEVEL, $name, $($field)* ) }; ($name:expr) => { $crate::otel_trace_span!($name,) }; } #[inline] #[must_use] pub fn find_current_context() -> Context { use tracing_opentelemetry::OpenTelemetrySpanExt; // let context = opentelemetry::Context::current(); // OpenTelemetry Context is propagation inside code is done via tracing crate tracing::Span::current().context() } /// Search the current opentelemetry trace id into the Context from the current tracing'span. /// This function can be used to report the trace id into the error message send back to user. /// /// ```rust /// let trace_id = tracing_opentelemetry_instrumentation_sdk::find_current_trace_id(); /// // json!({ "error" : "xxxxxx", "trace_id": trace_id}) /// /// ``` #[inline] #[must_use] pub fn find_current_trace_id() -> Option { find_trace_id(&find_current_context()) } #[inline] #[must_use] pub fn find_context_from_tracing(span: &tracing::Span) -> Context { use tracing_opentelemetry::OpenTelemetrySpanExt; // let context = opentelemetry::Context::current(); // OpenTelemetry Context is propagation inside code is done via tracing crate span.context() } #[inline] #[must_use] pub fn find_trace_id_from_tracing(span: &tracing::Span) -> Option { use tracing_opentelemetry::OpenTelemetrySpanExt; // let context = opentelemetry::Context::current(); // OpenTelemetry Context is propagation inside code is done via tracing crate find_trace_id(&span.context()) } #[inline] #[must_use] pub fn find_trace_id(context: &Context) -> Option { use opentelemetry::trace::TraceContextExt; let span = context.span(); let span_context = span.span_context(); span_context .is_valid() .then(|| span_context.trace_id().to_string()) // #[cfg(not(any( // feature = "opentelemetry_0_17", // feature = "opentelemetry_0_18", // feature = "opentelemetry_0_19" // )))] // let trace_id = span.context().span().span_context().trace_id().to_hex(); // #[cfg(any( // feature = "opentelemetry_0_17", // feature = "opentelemetry_0_18", // feature = "opentelemetry_0_19" // ))] // let trace_id = { // let id = span.context().span().span_context().trace_id(); // format!("{:032x}", id) // }; } #[inline] #[must_use] pub fn find_span_id(context: &Context) -> Option { use opentelemetry::trace::TraceContextExt; let span = context.span(); let span_context = span.span_context(); span_context .is_valid() .then(|| span_context.span_id().to_string()) } // pub(crate) fn set_otel_parent(parent_context: Context, span: &tracing::Span) { // use opentelemetry::trace::TraceContextExt as _; // use tracing_opentelemetry::OpenTelemetrySpanExt as _; // // let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| { // // propagator.extract(&RequestHeaderCarrier::new(req.headers())) // // }); // span.set_parent(parent_context); // // If we have a remote parent span, this will be the parent's trace identifier. // // If not, it will be the newly generated trace identifier with this request as root span. // if let Some(trace_id) = find_trace_id_from_tracing(&span) { // span.record("trace_id", trace_id); // } // } pub type BoxError = Box; ================================================ FILE: tracing-opentelemetry-instrumentation-sdk/src/span_type.rs ================================================ use std::fmt::Display; // SpanType is a non official open-telemetry key, only supported by Datadog, to help categorize traces. // Documentation: https://github.com/open-telemetry/opentelemetry-rust/blob/ccb510fbd6fdef9694e3b751fd01dbe33c7345c0/opentelemetry-datadog/src/lib.rs#L29-L30 // Usage: It should be informed as span.type span key // Reference: https://github.com/DataDog/dd-trace-go/blob/352b090d4f90527d35a8ad535b97689e346589c8/ddtrace/ext/app_types.go#L31-L81 #[allow(dead_code)] pub enum SpanType { Web, Http, Sql, Cassandra, Redis, Memcached, Mongodb, Elasticsearch, Leveldb, Dns, Queue, Consul, Graphql, } impl Display for SpanType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { SpanType::Web => "web", SpanType::Http => "http", SpanType::Sql => "sql", SpanType::Cassandra => "cassandra", SpanType::Redis => "redis", SpanType::Memcached => "memcached", SpanType::Mongodb => "mongodb", SpanType::Elasticsearch => "elasticsearch", SpanType::Leveldb => "leveldb", SpanType::Dns => "dns", SpanType::Queue => "queue", SpanType::Consul => "consul", SpanType::Graphql => "graphql", }; f.write_str(s) } }