├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .github ├── dco.yml ├── dependabot.yml └── workflows │ ├── close-stale-issues.yml │ ├── dependabot-automerge.yml │ ├── deploy-docs.yml │ ├── gradle-wrapper-validation.yml │ ├── post-release-workflow.yml │ └── process_changelog.py ├── .gitignore ├── .springjavaformatconfig ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SUPPORT.adoc ├── build.gradle ├── config └── checkstyle │ ├── checkstyle-suppressions.xml │ ├── checkstyle.xml │ └── suppressions.xml ├── context-propagation ├── build.gradle └── src │ ├── main │ └── java │ │ └── io │ │ └── micrometer │ │ └── context │ │ ├── ContextAccessor.java │ │ ├── ContextExecutorService.java │ │ ├── ContextRegistry.java │ │ ├── ContextScheduledExecutorService.java │ │ ├── ContextSnapshot.java │ │ ├── ContextSnapshotFactory.java │ │ ├── DefaultContextSnapshot.java │ │ ├── DefaultContextSnapshotFactory.java │ │ ├── NonNullApi.java │ │ ├── NonNullFields.java │ │ ├── Nullable.java │ │ ├── ThreadLocalAccessor.java │ │ ├── integration │ │ ├── Slf4jThreadLocalAccessor.java │ │ └── package-info.java │ │ └── package-info.java │ └── test │ └── java │ └── io │ └── micrometer │ ├── context │ ├── AnotherTestContextAccessor.java │ ├── ContextRegistryTests.java │ ├── ContextWrappingTests.java │ ├── DefaultContextSnapshotDepreactionTests.java │ ├── DefaultContextSnapshotTests.java │ ├── ScopedValueSnapshotTests.java │ ├── StringThreadLocalAccessor.java │ ├── StringThreadLocalHolder.java │ ├── TestContextAccessor.java │ ├── TestThreadLocalAccessor.java │ └── integration │ │ └── Slf4jThreadLocalAccessorTests.java │ └── scopedvalue │ ├── Scope.java │ ├── ScopeHolder.java │ ├── ScopedValue.java │ ├── ScopedValueTest.java │ └── ScopedValueThreadLocalAccessor.java ├── dependencies.gradle ├── docs ├── antora-playbook.yml ├── antora.yml ├── build.gradle ├── modules │ └── ROOT │ │ ├── examples │ │ └── docs-src │ │ ├── nav.adoc │ │ └── pages │ │ ├── index.adoc │ │ ├── installing.adoc │ │ ├── purpose.adoc │ │ └── usage.adoc └── src │ └── test │ └── java │ └── io │ └── micrometer │ └── docs │ └── context │ ├── DefaultContextSnapshotTests.java │ ├── ObservationThreadLocalAccessor.java │ └── ObservationThreadLocalHolder.java ├── gradle.properties ├── gradle ├── deploy.sh ├── libs.versions.toml ├── licenseHeader.txt └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | circle-jdk23-executor: 5 | working_directory: ~/context-propagation 6 | environment: 7 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' 8 | docker: 9 | - image: cimg/openjdk:23.0.2 10 | circle-jdk-executor: 11 | working_directory: ~/context-propagation 12 | environment: 13 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' 14 | docker: 15 | - image: cimg/openjdk:21.0.6 16 | circle-jdk11-executor: 17 | working_directory: ~/context-propagation 18 | environment: 19 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' 20 | docker: 21 | - image: cimg/openjdk:11.0.26 22 | circle-jdk17-executor: 23 | working_directory: ~/context-propagation 24 | environment: 25 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' 26 | docker: 27 | - image: cimg/openjdk:17.0.14 28 | machine-executor: 29 | working_directory: ~/context-propagation 30 | machine: 31 | image: ubuntu-2404:2024.11.1 32 | 33 | commands: 34 | gradlew-build: 35 | description: 'Run a Gradle build using the wrapper' 36 | parameters: 37 | command: 38 | type: string 39 | default: 'build' 40 | steps: 41 | - checkout 42 | - restore_cache: 43 | key: gradle-dependencies-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }} 44 | - run: 45 | name: downloadDependencies 46 | command: ./gradlew downloadDependencies --console=plain 47 | - save_cache: 48 | key: gradle-dependencies-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }} 49 | paths: 50 | - ~/.gradle 51 | - run: 52 | name: run gradle command 53 | command: ./gradlew << parameters.command >> 54 | - run: 55 | name: collect test reports 56 | when: always 57 | command: | 58 | mkdir -p ~/context-propagation/test-results/junit/ 59 | find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/context-propagation/test-results/junit/ \; 60 | - store_test_results: 61 | path: ~/context-propagation/test-results/ 62 | - store_artifacts: 63 | path: ~/context-propagation/test-results/ 64 | 65 | jobs: 66 | build-jdk23: 67 | executor: circle-jdk23-executor 68 | steps: 69 | - gradlew-build 70 | 71 | build: 72 | executor: circle-jdk-executor 73 | steps: 74 | - gradlew-build 75 | 76 | build-jdk11: 77 | executor: circle-jdk11-executor 78 | steps: 79 | - gradlew-build 80 | 81 | build-jdk17: 82 | executor: circle-jdk17-executor 83 | steps: 84 | - gradlew-build 85 | 86 | deploy: 87 | executor: circle-jdk-executor 88 | steps: 89 | - checkout 90 | - restore_cache: 91 | key: gradle-dependencies-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }} 92 | - run: 93 | name: Deployment 94 | command: sh ./gradle/deploy.sh 95 | 96 | workflows: 97 | version: 2 98 | build_prs_deploy_snapshots: 99 | jobs: 100 | - build 101 | - build-jdk11 102 | - build-jdk17 103 | - build-jdk23 104 | - deploy: 105 | context: 106 | - deploy 107 | requires: 108 | - build 109 | - build-jdk11 110 | - build-jdk17 111 | - build-jdk23 112 | filters: 113 | branches: 114 | only: 115 | - main 116 | - /\d+\.\d+\.x/ 117 | build_deploy_releases: 118 | jobs: 119 | - build: 120 | filters: 121 | branches: 122 | ignore: /.*/ 123 | tags: 124 | only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ 125 | - build-jdk11: 126 | filters: 127 | branches: 128 | ignore: /.*/ 129 | tags: 130 | only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ 131 | - build-jdk17: 132 | filters: 133 | branches: 134 | ignore: /.*/ 135 | tags: 136 | only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ 137 | - build-jdk23: 138 | filters: 139 | branches: 140 | ignore: /.*/ 141 | tags: 142 | only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ 143 | - deploy: 144 | context: 145 | - deploy 146 | requires: 147 | - build 148 | - build-jdk11 149 | - build-jdk17 150 | - build-jdk23 151 | filters: 152 | tags: 153 | only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ 154 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | 6 | [*.java] 7 | indent_style = space 8 | indent_size = 4 9 | continuation_indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lockfile linguist-generated -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | target-branch: "1.0.x" # oldest supported branch 6 | schedule: 7 | interval: "weekly" 8 | # Non-build dependencies; target every supported branch 9 | - package-ecosystem: gradle 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | target-branch: "1.0.x" 14 | milestone: 27 15 | ignore: 16 | # only upgrade patch versions 17 | - dependency-name: "*" 18 | update-types: 19 | - version-update:semver-major 20 | - version-update:semver-minor 21 | open-pull-requests-limit: 10 22 | - package-ecosystem: gradle 23 | directory: "/" 24 | schedule: 25 | interval: daily 26 | target-branch: "1.1.x" 27 | milestone: 16 28 | ignore: 29 | # only upgrade patch versions 30 | - dependency-name: "*" 31 | update-types: 32 | - version-update:semver-major 33 | - version-update:semver-minor 34 | open-pull-requests-limit: 10 35 | - package-ecosystem: gradle 36 | directory: "/" 37 | schedule: 38 | interval: daily 39 | target-branch: "main" 40 | milestone: 21 41 | ignore: 42 | # upgrade minor and patch versions on main 43 | - dependency-name: "*" 44 | update-types: 45 | - version-update:semver-major 46 | open-pull-requests-limit: 10 47 | -------------------------------------------------------------------------------- /.github/workflows/close-stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | workflow_dispatch: 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | stale-issue-message: 'If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.' 14 | stale-pr-message: 'If you would like us to look at this PR, please provide the requested information. If the information is not provided within the next 7 days this PR will be closed.' 15 | close-issue-message: 'Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open.' 16 | close-pr-message: 'Closing due to lack of requested feedback. If you would like us to look at this, please provide the requested information and we will re-open.' 17 | close-issue-label: 'closed-as-inactive' 18 | days-before-stale: 7 19 | days-before-close: 7 20 | stale-issue-label: 'feedback-reminder' 21 | stale-pr-label: 'feedback-reminder' 22 | only-labels: 'waiting for feedback' 23 | exempt-issue-labels: 'feedback-provided' 24 | exempt-pr-labels: 'feedback-provided' 25 | exempt-all-milestones: true 26 | operations-per-run: 300 27 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | name: Merge Dependabot PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - '*.x' 8 | 9 | run-name: Merge Dependabot PR ${{ github.ref_name }} 10 | 11 | jobs: 12 | merge-dependabot-pr: 13 | permissions: write-all 14 | 15 | # Until v6 is released 16 | uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@b00736028017be7e964ed0a37c907ae1bf833f39 17 | with: 18 | autoMerge: true 19 | mergeArguments: --auto --squash 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | on: 3 | push: 4 | branches: [ main, '[0-9]+.[0-9]+.x' ] 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | - 'v[0-9]+.[0-9]+.0-M[0-9]' 8 | - 'v[0-9]+.[0-9]+.0-RC[0-9]' 9 | repository_dispatch: 10 | types: request-build-reference # legacy 11 | #schedule: 12 | #- cron: '0 10 * * *' # Once per day at 10am UTC 13 | workflow_dispatch: 14 | permissions: 15 | actions: write 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | ref: docs-build 24 | fetch-depth: 1 25 | - name: Dispatch (partial build) 26 | if: github.ref_type == 'branch' 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} 30 | - name: Dispatch (full build) 31 | if: github.ref_type == 'tag' 32 | env: 33 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) 35 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push, pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | validation: 9 | name: "Validation" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: gradle/actions/wrapper-validation@v4 14 | -------------------------------------------------------------------------------- /.github/workflows/post-release-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Post Release Workflow 2 | 3 | on: 4 | workflow_dispatch: # Enables manual trigger 5 | 6 | jobs: 7 | generate-release-notes: 8 | name: Generate Release Notes 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Download Changelog Generator 16 | run: | 17 | curl -L -o github-changelog-generator.jar https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.11/github-changelog-generator.jar 18 | 19 | - name: Generate release notes 20 | id: generate_notes 21 | run: | 22 | java -jar github-changelog-generator.jar \ 23 | ${GITHUB_REF_NAME#v} \ 24 | changelog.md \ 25 | --changelog.repository="${{ github.repository }}" \ 26 | --github.token="${{ secrets.GITHUB_TOKEN }}" 27 | 28 | - name: Run script to process Markdown file 29 | run: python .github/workflows/process_changelog.py 30 | 31 | - name: Update release text 32 | run: | 33 | echo -e "::Info::Original changelog\n\n" 34 | cat changelog.md 35 | 36 | echo -e "\n\n" 37 | echo -e "::Info::Processed changelog\n\n" 38 | cat changelog-output.md 39 | gh release edit ${{ github.ref_name }} --notes-file changelog-output.md 40 | env: 41 | GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} 42 | 43 | close-milestone: 44 | name: Close Milestone 45 | runs-on: ubuntu-latest 46 | needs: generate-release-notes 47 | steps: 48 | - name: Close milestone 49 | run: | 50 | # Extract version without 'v' prefix 51 | milestone_name=${GITHUB_REF_NAME#v} 52 | 53 | echo "Closing milestone: $milestone_name" 54 | 55 | # List milestones and find the ID 56 | milestone_id=$(gh api "/repos/${{ github.repository }}/milestones?state=open" \ 57 | --jq ".[] | select(.title == \"$milestone_name\").number") 58 | 59 | if [ -z "$milestone_id" ]; then 60 | echo "::error::Milestone '$milestone_name' not found" 61 | exit 1 62 | fi 63 | 64 | # Close the milestone 65 | gh api --method PATCH "/repos/${{ github.repository }}/milestones/$milestone_id" \ 66 | -f state=closed 67 | 68 | echo "Successfully closed milestone: $milestone_name" 69 | env: 70 | GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} 71 | 72 | notify: 73 | name: Send Notifications 74 | runs-on: ubuntu-latest 75 | needs: close-milestone 76 | 77 | steps: 78 | - name: Announce Release on `Spring-Releases` space 79 | run: | 80 | milestone_name=${GITHUB_REF_NAME#v} 81 | curl --location --request POST '${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }}' \ 82 | --header 'Content-Type: application/json' \ 83 | --data-raw "{ text: \"${{ github.event.repository.name }}-announcing ${milestone_name}\"}" 84 | 85 | - name: Post on Bluesky 86 | env: 87 | BSKY_IDENTIFIER: ${{ secrets.BLUESKY_HANDLE }} 88 | BSKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} 89 | run: | 90 | # First get the session token 91 | SESSION_TOKEN=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \ 92 | -H "Content-Type: application/json" \ 93 | -d "{\"identifier\":\"$BSKY_IDENTIFIER\",\"password\":\"$BSKY_PASSWORD\"}" | \ 94 | jq -r .accessJwt) 95 | 96 | # Create post content 97 | VERSION=${GITHUB_REF_NAME#v} 98 | POST_TEXT="${{ github.event.repository.name }} ${VERSION} has been released!\n\nCheck out the changelog: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}" 99 | 100 | # Create the post 101 | curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ 102 | -H "Content-Type: application/json" \ 103 | -H "Authorization: Bearer ${SESSION_TOKEN}" \ 104 | -d "{ 105 | \"repo\": \"$BSKY_IDENTIFIER\", 106 | \"collection\": \"app.bsky.feed.post\", 107 | \"record\": { 108 | \"\$type\": \"app.bsky.feed.post\", 109 | \"text\": \"$POST_TEXT\", 110 | \"createdAt\": \"$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")\" 111 | } 112 | }" 113 | -------------------------------------------------------------------------------- /.github/workflows/process_changelog.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | 4 | input_file = "changelog.md" 5 | output_file = "changelog-output.md" 6 | 7 | def fetch_test_and_optional_dependencies(): 8 | # Fetch the list of all subprojects 9 | result = subprocess.run( 10 | ["./gradlew", "projects"], 11 | stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE, 13 | text=True, 14 | ) 15 | subprojects = [] 16 | for line in result.stdout.splitlines(): 17 | match = re.match(r".*Project (':.+')", line) 18 | if match: 19 | subprojects.append(match.group(1).strip("'")) 20 | 21 | print(f"Found the following subprojects\n\n {subprojects}\n\n") 22 | test_optional_dependencies = set() 23 | implementation_dependencies = set() 24 | 25 | print("Will fetch non transitive dependencies for all subprojects...") 26 | # Run dependencies task for all subprojects in a single Gradle command 27 | if subprojects: 28 | dependencies_command = ["./gradlew"] + [f"{subproject}:dependencies" for subproject in subprojects] 29 | result = subprocess.run( 30 | dependencies_command, 31 | stdout=subprocess.PIPE, 32 | stderr=subprocess.PIPE, 33 | text=True, 34 | ) 35 | in_test_section = False 36 | in_optional_section = False 37 | in_implementation_section = False 38 | 39 | for line in result.stdout.splitlines(): 40 | if "project :" in line: 41 | continue 42 | 43 | # Detect gradle plugin 44 | if "classpath" in line: 45 | in_optional_section = True 46 | continue 47 | 48 | # Detect test dependencies section 49 | if "testCompileClasspath" in line or "testImplementation" in line: 50 | in_test_section = True 51 | continue 52 | if "runtimeClasspath" in line or line.strip() == "": 53 | in_test_section = False 54 | 55 | # Detect optional dependencies section 56 | if "compileOnly" in line: 57 | in_optional_section = True 58 | continue 59 | if line.strip() == "": 60 | in_optional_section = False 61 | 62 | # Detect implementation dependencies section 63 | if "implementation" in line or "compileClasspath" in line: 64 | in_implementation_section = True 65 | continue 66 | if line.strip() == "": 67 | in_implementation_section = False 68 | 69 | # Parse dependencies explicitly declared with +--- or \--- 70 | match = re.match(r"[\\+|\\\\]--- ([^:]+):([^:]+):([^ ]+)", line) 71 | if match: 72 | group_id, artifact_id, _ = match.groups() 73 | dependency_key = f"{group_id}:{artifact_id}" 74 | if in_test_section or in_optional_section: 75 | test_optional_dependencies.add(dependency_key) 76 | if in_implementation_section: 77 | implementation_dependencies.add(dependency_key) 78 | 79 | # Remove dependencies from test/optional if they are also in implementation 80 | final_exclusions = test_optional_dependencies - implementation_dependencies 81 | 82 | print(f"Dependencies in either test or optional scope to be excluded from changelog processing:\n\n{final_exclusions}\n\n") 83 | return final_exclusions 84 | 85 | def process_dependency_upgrades(lines, exclude_dependencies): 86 | dependencies = {} 87 | regex = re.compile(r"- Bump (.+?) from ([\d\.]+) to ([\d\.]+) \[(#[\d]+)\]\((.+)\)") 88 | for line in lines: 89 | match = regex.match(line) 90 | if match: 91 | unit, old_version, new_version, pr_number, link = match.groups() 92 | if unit not in exclude_dependencies: 93 | if unit not in dependencies: 94 | dependencies[unit] = {"lowest": old_version, "highest": new_version, "pr_number": pr_number, "link": link} 95 | else: 96 | dependencies[unit]["lowest"] = min(dependencies[unit]["lowest"], old_version) 97 | dependencies[unit]["highest"] = max(dependencies[unit]["highest"], new_version) 98 | sorted_units = sorted(dependencies.keys()) 99 | return [f"- Bump {unit} from {dependencies[unit]['lowest']} to {dependencies[unit]['highest']} [{dependencies[unit]['pr_number']}]({dependencies[unit]['link']})" for unit in sorted_units] 100 | 101 | with open(input_file, "r") as file: 102 | lines = file.readlines() 103 | 104 | # Fetch test and optional dependencies from all projects 105 | print("Fetching test and optional dependencies from the project and its subprojects...") 106 | exclude_dependencies = fetch_test_and_optional_dependencies() 107 | 108 | # Step 1: Copy all content until the hammer line 109 | header = [] 110 | dependency_lines = [] 111 | footer = [] 112 | in_dependency_section = False 113 | 114 | print("Parsing changelog until the dependency upgrades section...") 115 | 116 | for line in lines: 117 | if line.startswith("## :hammer: Dependency Upgrades"): 118 | in_dependency_section = True 119 | header.append(line) 120 | header.append("\n") 121 | break 122 | header.append(line) 123 | 124 | print("Parsing dependency upgrade section...") 125 | 126 | # Step 2: Parse dependency upgrades 127 | if in_dependency_section: 128 | for line in lines[len(header):]: 129 | if line.startswith("## :heart: Contributors"): 130 | break 131 | dependency_lines.append(line) 132 | 133 | print("Parsing changelog to find everything after the dependency upgrade section...") 134 | # Find the footer starting from the heart line 135 | footer_start_index = next((i for i, line in enumerate(lines) if line.startswith("## :heart: Contributors")), None) 136 | if footer_start_index is not None: 137 | footer = lines[footer_start_index:] 138 | 139 | print("Processing the dependency upgrades section...") 140 | processed_dependencies = process_dependency_upgrades(dependency_lines, exclude_dependencies) 141 | 142 | print("Writing output...") 143 | # Step 3: Write the output file 144 | with open(output_file, "w") as file: 145 | file.writelines(header) 146 | file.writelines(f"{line}\n" for line in processed_dependencies) 147 | file.writelines("\n") 148 | file.writelines(footer) 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | out/ 4 | 5 | code/ 6 | 7 | *.iml 8 | *.ipr 9 | *.iws 10 | .idea 11 | 12 | .classpath 13 | .project 14 | .settings/ 15 | .sts4-cache/ 16 | bin/ 17 | .factorypath 18 | .vscode 19 | .DS_Store 20 | .java-version 21 | -------------------------------------------------------------------------------- /.springjavaformatconfig: -------------------------------------------------------------------------------- 1 | java-baseline=8 2 | indentation-style=spaces 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | This Contributing Guide is intended for those that would like to contribute to Micrometer Context Propagation. 4 | 5 | If you would like to use any of the published Micrometer Context Propagation modules as a library in your project, you can instead 6 | include the Micrometer Context Propagation artifacts from the Maven Central repository using your build tool of choice. 7 | 8 | ## Code of Conduct 9 | 10 | See [our Contributor Code of Conduct](https://github.com/micrometer-metrics/.github/blob/main/CODE_OF_CONDUCT.md). 11 | 12 | ## Contributions 13 | 14 | Contributions come in various forms and are not limited to code changes. The Micrometer Context Propagation community benefits from 15 | contributions in all forms. 16 | 17 | For example, those with Micrometer Context Propagation knowledge and experience can contribute by: 18 | 19 | * TODO: [Contributing documentation]() 20 | * Answering [Stackoverflow questions](https://stackoverflow.com/tags/micrometer) 21 | * Answering questions on the [Micrometer slack](https://slack.micrometer.io) 22 | * Share Micrometer Context Propagation knowledge in other ways (e.g. presentations, blogs) 23 | 24 | The remainder of this document will focus on guidance for contributing code changes. It will help contributors to build, 25 | modify, or test the Micrometer Context Propagation source code. 26 | 27 | ## Include a Signed Off By Trailer 28 | 29 | All commits must include a *Signed-off-by* trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin](https://developercertificate.org). 30 | For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). 31 | 32 | ## Getting the source 33 | 34 | The Micrometer Context Propagation source code is hosted on GitHub at https://github.com/micrometer-metrics/context-propagation. You can use a 35 | Git client to clone the source code to your local machine. 36 | 37 | ## Building 38 | 39 | Micrometer Context Propagation targets Java 8 but requires JDK 11 or later to build. 40 | 41 | The Gradle wrapper is provided and should be used for building with a consistent version of Gradle. 42 | 43 | The wrapper can be used with a command, for example, `./gradlew check` to build the project and check conventions. 44 | 45 | ## Importing into an IDE 46 | 47 | This repository should be imported as a Gradle project into your IDE of choice. 48 | 49 | ## Testing changes locally 50 | 51 | Specific modules or a test class can be run from your IDE for convenience. 52 | 53 | The Gradle `check` task depends on the `test` task, and so tests will be run as part of a build as described previously. 54 | 55 | ### Publishing local snapshots 56 | 57 | Run `./gradlew pTML` to publish a Maven-style snapshot to your Maven local repo. The build automatically calculates 58 | the "next" version for you when publishing snapshots. 59 | 60 | These local snapshots can be used in another project to test the changes. For example: 61 | 62 | ```groovy 63 | repositories { 64 | mavenLocal() 65 | } 66 | 67 | dependencies { 68 | implementation 'io.micrometer:context-propagation:latest.integration' 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Micrometer Context Propagation 2 | 3 | Copyright (c) 2017-Present VMware, Inc. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | ------------------------------------------------------------------------------- 18 | 19 | This product contains a modified portion of 'io.netty.util.internal.logging', 20 | in the Netty/Common library distributed by The Netty Project: 21 | 22 | * Copyright 2013 The Netty Project 23 | * License: Apache License v2.0 24 | * Homepage: https://netty.io 25 | 26 | This product contains a modified portion of 'StringUtils.isBlank()', 27 | in the Commons Lang library distributed by The Apache Software Foundation: 28 | 29 | * Copyright 2001-2019 The Apache Software Foundation 30 | * License: Apache License v2.0 31 | * Homepage: https://commons.apache.org/proper/commons-lang/ 32 | 33 | This product contains a modified portion of 'JsonUtf8Writer', 34 | in the Moshi library distributed by Square, Inc: 35 | 36 | * Copyright 2010 Google Inc. 37 | * License: Apache License v2.0 38 | * Homepage: https://github.com/square/moshi 39 | 40 | This product contains a modified portion of the 'org.springframework.lang' 41 | package in the Spring Framework library, distributed by VMware, Inc: 42 | 43 | * Copyright 2002-2019 the original author or authors. 44 | * License: Apache License v2.0 45 | * Homepage: https://spring.io/projects/spring-framework 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Context Propagation Library 2 | 3 | [![Build Status](https://circleci.com/gh/micrometer-metrics/context-propagation.svg?style=shield)](https://circleci.com/gh/micrometer-metrics/context-propagation) 4 | [![Apache 2.0](https://img.shields.io/github/license/micrometer-metrics/context-propagation.svg)](https://www.apache.org/licenses/LICENSE-2.0) 5 | [![Maven Central](https://img.shields.io/maven-central/v/io.micrometer/context-propagation.svg)](https://search.maven.org/artifact/io.micrometer/context-propagation) 6 | [![Javadocs](https://www.javadoc.io/badge/io.micrometer/context-propagation.svg)](https://www.javadoc.io/doc/io.micrometer/context-propagation) 7 | [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.micrometer.io/) 8 | 9 | ## Overview 10 | 11 | A library that assists with context propagation across different types of context 12 | mechanisms such as `ThreadLocal`, Reactor [Context](https://projectreactor.io/docs/core/release/reference/#context), 13 | and others. 14 | 15 | Abstractions: 16 | 17 | * `ThreadLocalAccessor` - contract to assist with access to a `ThreadLocal` value. 18 | * `ContextAccessor` - contract to assist with access to a `Map`-like context. 19 | * `ContextRegistry` - registry for instances of `ThreadLocalAccessor` and `ContextAccessor`. 20 | * `ContextSnapshot` - holder of contextual values, that provides methods to capture and to propagate. 21 | 22 | You can read the full [reference documentation here](https://docs.micrometer.io/context-propagation/reference/). 23 | 24 | Example Scenarios: 25 | 26 | * In imperative code, e.g. Spring MVC controller, capture `ThreadLocal` values into a 27 | `ContextSnapshot`. After that use the snapshot to populate a Reactor `Context` with the 28 | captured values, or to wrap a task (e.g. `Runnable`, `Callable`, etc) or an `Executor` 29 | with a decorator that restores `ThreadLocal` values when the task executes. 30 | * In reactive code, e.g. Spring WebFlux controller, create a `ContextSnapshot` from 31 | Reactor `Context` values. After that use the snapshot to restore `ThreadLocal` values 32 | within a specific stage (operator) of the reactive chain. 33 | 34 | Context values can originate from any context mechanism and propagate to any other, any 35 | number of times. For example, a value in a `Reactor` context may originate as a 36 | `ThreadLocal`, and may yet become a `ThreadLocal` again, and so on. 37 | 38 | Generally, imperative code should interact with `ThreadLocal` values as usual, and 39 | likewise Reactor code should interact with the Reactor `Context` as usual. The Context 40 | Propagation library is not intended to replace those, but to assist with propagation when 41 | crossing from one type of context to another, e.g. when imperative code invokes a Reactor 42 | chain, or when a Reactor chain invokes an imperative component that expects 43 | `ThreadLocal` values. 44 | 45 | The library is not limited to context propagation from imperative to reactive. It can 46 | assist in asynchronous scenarios to propagate `ThreadLocal` values from one thread to 47 | another. It can also propagate to any other type of context for which there is a 48 | registered `ContextAccesor` instance. 49 | 50 | ## Artifacts 51 | 52 | The published artifacts work with Java 8 or later. 53 | 54 | ### Snapshot builds 55 | 56 | Snapshots are published to `repo.spring.io` for every successful build on the `main` branch and maintenance branches. 57 | 58 | ```groovy 59 | repositories { 60 | maven { url 'https://repo.spring.io/snapshot' } 61 | } 62 | 63 | dependencies { 64 | implementation 'io.micrometer:context-propagation:latest.integration' 65 | } 66 | ``` 67 | 68 | ### Milestone releases 69 | 70 | Starting with the 1.2.0-M1 release, milestone releases and release candidates will be published to Maven Central. Note that milestone releases are for testing purposes and are not intended for production use. Earlier milestone releases were published to https://repo.spring.io/milestone. 71 | 72 | ## Contributing 73 | 74 | See our [Contributing Guide](CONTRIBUTING.md) for information about contributing to Micrometer Context Propagation. 75 | 76 | ## Code formatting 77 | 78 | The [spring-javaformat plugin](https://github.com/spring-io/spring-javaformat) is configured to check and apply consistent formatting in the codebase through the build. 79 | The `checkFormat` task checks the formatting as part of the `check` task. 80 | Apply formatting with the `format` task. 81 | You should rely on the formatting the `format` task applies instead of your IDE's configured formatting. 82 | 83 | ## Join the discussion 84 | 85 | Join the [Micrometer Slack](https://slack.micrometer.io) (#context-propagation channel) to share your questions, concerns, and feature requests. 86 | 87 | 88 | ------------------------------------- 89 | _Licensed under [Apache Software License 2.0](https://www.apache.org/licenses/LICENSE-2.0)_ 90 | 91 | _Sponsored by [VMware](https://tanzu.vmware.com)_ 92 | -------------------------------------------------------------------------------- /SUPPORT.adoc: -------------------------------------------------------------------------------- 1 | = Getting support for Context Propagation 2 | 3 | == GitHub issues 4 | We choose not to use GitHub issues for general usage questions and support, preferring to 5 | use issues solely for the tracking of bugs and enhancements. If you have a general 6 | usage question please do not open a GitHub issue, but use one of the other channels 7 | described below. 8 | 9 | If you are reporting a bug, please help to speed up problem diagnosis by providing as 10 | much information as possible. Ideally, that would include a small sample project that 11 | reproduces the problem. 12 | 13 | == Stack Overflow 14 | The Micrometer community monitors the 15 | https://stackoverflow.com/tags/micrometer[`micrometer`] tag on Stack Overflow. Before 16 | asking a question, please familiarize yourself with Stack Overflow's 17 | https://stackoverflow.com/help/how-to-ask[advice on how to ask a good question]. 18 | 19 | == Slack 20 | If you want to discuss something or have a question that isn't suited to Stack Overflow, 21 | the Micrometer community chat in the https://slack.micrometer.io[micrometer-metrics Slack]. 22 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | 3 | repositories { 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | 8 | dependencies { 9 | classpath libs.plugin.license 10 | classpath libs.plugin.nebulaRelease 11 | classpath libs.plugin.nebulaPublishing 12 | classpath libs.plugin.nebulaProject 13 | classpath libs.plugin.noHttp 14 | classpath libs.plugin.nexusPublish 15 | classpath libs.plugin.javaformat 16 | 17 | constraints { 18 | classpath('org.ow2.asm:asm:7.3.1') { 19 | because 'Supports modern JDKs' 20 | } 21 | } 22 | } 23 | 24 | configurations.classpath.resolutionStrategy.cacheDynamicVersionsFor 0, 'minutes' 25 | } 26 | 27 | // Hacks because of Antora's clone/checkout/worktrees behavior 28 | // Antora uses shallow-clone and worktrees to check out branches/tags. 29 | if (project.hasProperty('antora')) { 30 | 'git fetch --unshallow --all --tags'.execute().text // Antora shallow-clones so there is no history (we need commit history to find the last tag in the tree) 31 | String ref = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() 32 | if (ref == 'HEAD') { // if Antora checks out a tag instead of a branch 33 | String tag = 'git tag --points-at HEAD'.execute().text.trim() // jgit is not able to figure out tags in Antora's worktree 34 | if (tag) { 35 | println "Found release tag: $tag, using it as release.version" 36 | ext['release.version'] = tag.substring(1) 37 | } 38 | } 39 | } 40 | 41 | // TODO: remove this hack, see: https://github.com/nebula-plugins/nebula-release-plugin/issues/213 42 | def releaseStage = findProperty('release.stage') 43 | apply plugin: 'nebula.release' 44 | release.defaultVersionStrategy = nebula.plugin.release.git.opinion.Strategies.SNAPSHOT 45 | 46 | apply plugin: 'io.github.gradle-nexus.publish-plugin' 47 | apply from: 'dependencies.gradle' 48 | 49 | allprojects { 50 | group = 'io.micrometer' 51 | ext.'release.stage' = releaseStage ?: 'SNAPSHOT' 52 | 53 | afterEvaluate { project -> println "I'm configuring $project.name with version $project.version" } 54 | } 55 | 56 | subprojects { 57 | apply plugin: 'signing' 58 | apply plugin: 'io.spring.javaformat' 59 | apply plugin: 'java-library' 60 | apply plugin: 'com.github.hierynomus.license' 61 | apply plugin: 'checkstyle' 62 | apply plugin: 'io.spring.nohttp' 63 | 64 | java { 65 | // It is more idiomatic to define different features for different sets of optional 66 | // dependencies, e.g., 'dropwizard' and 'reactor'. If this library published Gradle 67 | // metadata, Gradle users would be able to use these feature names in their dependency 68 | // declarations instead of understanding the actual required optional dependencies. 69 | // But we don't publish Gradle metadata yet and this may be overkill so just have a 70 | // single feature for now to correspond to any optional dependency. 71 | registerFeature('optional') { 72 | usingSourceSet(sourceSets.main) 73 | } 74 | } 75 | 76 | // All projects use optional annotations, but since we don't expose them downstream we would 77 | // have to add the dependency in every project, which is tedious so just do it here. 78 | dependencies { 79 | // JSR-305 only used for non-required meta-annotations 80 | optionalApi libs.jsr305 81 | checkstyle libs.javaFormatForPlugins 82 | } 83 | 84 | tasks { 85 | compileJava { 86 | options.encoding = 'UTF-8' 87 | options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' 88 | 89 | sourceCompatibility = JavaVersion.VERSION_1_8 90 | targetCompatibility = JavaVersion.VERSION_1_8 91 | 92 | // ensure Java 8 baseline is enforced for main source 93 | if (JavaVersion.current().isJava9Compatible()) { 94 | options.release = 8 95 | } 96 | } 97 | compileTestJava { 98 | options.encoding = 'UTF-8' 99 | options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' 100 | sourceCompatibility = JavaVersion.VERSION_1_8 101 | targetCompatibility = JavaVersion.VERSION_1_8 102 | } 103 | 104 | javadoc { 105 | configure(options) { 106 | tags( 107 | 'apiNote:a:API Note:', 108 | 'implSpec:a:Implementation Requirements:', 109 | 'implNote:a:Implementation Note:' 110 | ) 111 | } 112 | } 113 | } 114 | 115 | normalization { 116 | runtimeClasspath { 117 | metaInf { 118 | [ 119 | 'Build-Date', 120 | 'Build-Date-UTC', 121 | 'Built-By', 122 | 'Built-OS', 123 | 'Build-Host', 124 | 'Build-Job', 125 | 'Build-Number', 126 | 'Build-Id', 127 | 'Change', 128 | 'Full-Change', 129 | 'Branch', 130 | 'Module-Origin', 131 | 'Created-By', 132 | 'Build-Java-Version' 133 | ].each { 134 | ignoreAttribute it 135 | ignoreProperty it 136 | } 137 | } 138 | } 139 | } 140 | 141 | //noinspection GroovyAssignabilityCheck 142 | test { 143 | // set heap size for the test JVM(s) 144 | maxHeapSize = '1500m' 145 | 146 | useJUnitPlatform { 147 | excludeTags 'docker' 148 | } 149 | 150 | develocity.testRetry { 151 | maxFailures = 5 152 | maxRetries = 3 153 | } 154 | } 155 | 156 | project.tasks.withType(Test) { Test testTask -> 157 | testTask.testLogging.exceptionFormat = 'full' 158 | } 159 | 160 | license { 161 | header rootProject.file('gradle/licenseHeader.txt') 162 | strictCheck true 163 | mapping { 164 | kt = 'SLASHSTAR_STYLE' 165 | } 166 | sourceSets = project.sourceSets 167 | 168 | ext.year = Calendar.getInstance().get(Calendar.YEAR) 169 | skipExistingHeaders = true 170 | exclude '**/*.json' // comments not supported 171 | } 172 | 173 | // Publish resolved versions. 174 | plugins.withId('maven-publish') { 175 | sourceCompatibility = JavaVersion.VERSION_1_8 176 | targetCompatibility = JavaVersion.VERSION_1_8 177 | 178 | publishing { 179 | publications { 180 | nebula(MavenPublication) { 181 | versionMapping { 182 | allVariants { 183 | fromResolutionResult() 184 | } 185 | } 186 | 187 | // We publish resolved versions so don't need to publish our dependencyManagement 188 | // too. This is different from many Maven projects, where published artifacts often 189 | // don't include resolved versions and have a parent POM including dependencyManagement. 190 | pom.withXml { 191 | def dependencyManagement = asNode().get('dependencyManagement') 192 | if (dependencyManagement != null) { 193 | asNode().remove(dependencyManagement) 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | plugins.withId('maven-publish') { 202 | publishing { 203 | publications { 204 | nebula(MavenPublication) { 205 | // Nebula converts dynamic versions to static ones so it's ok. 206 | suppressAllPomMetadataWarnings() 207 | } 208 | } 209 | repositories { 210 | maven { 211 | name = 'Snapshot' 212 | url = 'https://repo.spring.io/snapshot' 213 | credentials { 214 | username findProperty('SNAPSHOT_REPO_USER') 215 | password findProperty('SNAPSHOT_REPO_PASSWORD') 216 | } 217 | } 218 | } 219 | } 220 | 221 | signing { 222 | required = System.env.CIRCLE_STAGE == 'deploy' 223 | useInMemoryPgpKeys(findProperty('SIGNING_KEY'), findProperty('SIGNING_PASSWORD')) 224 | sign publishing.publications.nebula 225 | } 226 | 227 | // Nebula doesn't interface with Gradle's module format so just disable it for now. 228 | tasks.withType(GenerateModuleMetadata) { 229 | enabled = false 230 | } 231 | } 232 | 233 | tasks.register('downloadDependencies') { 234 | outputs.upToDateWhen { false } 235 | doLast { 236 | project.configurations.findAll { it.canBeResolved }*.files 237 | } 238 | } 239 | 240 | if (!['samples', 'benchmarks'].find { project.name.contains(it) }) { 241 | apply plugin: 'com.netflix.nebula.maven-publish' 242 | apply plugin: 'com.netflix.nebula.maven-manifest' 243 | apply plugin: 'com.netflix.nebula.maven-developer' 244 | apply plugin: 'com.netflix.nebula.javadoc-jar' 245 | apply plugin: 'com.netflix.nebula.source-jar' 246 | apply plugin: 'com.netflix.nebula.maven-apache-license' 247 | apply plugin: 'com.netflix.nebula.publish-verification' 248 | apply plugin: 'com.netflix.nebula.contacts' 249 | apply plugin: 'com.netflix.nebula.info' 250 | apply plugin: 'com.netflix.nebula.project' 251 | 252 | jar { 253 | manifest.attributes.put('Automatic-Module-Name', project.name.replace('-', '.')) 254 | metaInf { 255 | from "$rootDir/LICENSE" 256 | from "$rootDir/NOTICE" 257 | } 258 | } 259 | 260 | contacts { 261 | 'tludwig@vmware.com' { 262 | moniker 'Tommy Ludwig' 263 | github 'shakuzen' 264 | } 265 | 'jivanov@vmware.com' { 266 | moniker 'Jonatan Ivanov' 267 | github 'jonatan-ivanov' 268 | } 269 | 'mgrzejszczak@vmware.com' { 270 | moniker 'Marcin Grzejszczak' 271 | github 'marcingrzejszczak' 272 | } 273 | } 274 | } 275 | 276 | description = 'A library that assists with context propagation across different types of context mechanisms such as ThreadLocal, Reactor Context etc.' 277 | 278 | repositories { 279 | mavenCentral() 280 | } 281 | 282 | def check = tasks.findByName('check') 283 | if (check) project.rootProject.tasks.releaseCheck.dependsOn check 284 | } 285 | 286 | nexusPublishing { 287 | repositories { 288 | mavenCentral { 289 | nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/')) 290 | snapshotRepositoryUrl.set(uri('https://repo.spring.io/snapshot/')) // not used but necessary for the plugin 291 | username = findProperty('MAVEN_CENTRAL_USER') 292 | password = findProperty('MAVEN_CENTRAL_PASSWORD') 293 | } 294 | } 295 | } 296 | 297 | wrapper { 298 | gradleVersion = '8.14.2' 299 | } 300 | 301 | defaultTasks 'build' 302 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /config/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /context-propagation/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'idea' 3 | } 4 | 5 | dependencies { 6 | compileOnly libs.slf4j 7 | 8 | testImplementation libs.junitJupiter 9 | testRuntimeOnly libs.junitPlatformLauncher 10 | testImplementation libs.assertj 11 | testImplementation libs.slf4j 12 | testImplementation libs.logback 13 | } 14 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ContextAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Map; 19 | import java.util.function.Predicate; 20 | 21 | /** 22 | * Contract to assist with access to an external, map-like context, such as the Project 23 | * Reactor {@code Context}, including the ability to read values from it a {@link Map} and 24 | * to write values to it from a {@link Map}. 25 | * 26 | * @param type of context for reading 27 | * @param type of context for writing 28 | * @author Marcin Grzejszczak 29 | * @author Rossen Stoyanchev 30 | * @since 1.0.0 31 | */ 32 | public interface ContextAccessor { 33 | 34 | /** 35 | * {@link Class} representing the type of context this accessor is capable of reading 36 | * values from. 37 | */ 38 | Class readableType(); 39 | 40 | /** 41 | * Read values from a source context into a {@link Map}. 42 | * @param sourceContext the context to read from; the context type should be 43 | * {@link Class#isAssignableFrom(Class) assignable} from the type returned by 44 | * {@link #readableType()}. 45 | *

46 | * When an {@link ContextAccessor} is used to populate a {@link ContextSnapshot}, the 47 | * snapshot implementations are required to filter out {@code null} mappings, so it is 48 | * not required to implement special handling in the accessor. 49 | * @param keyPredicate a predicate to decide which keys to read 50 | * @param readValues a map where to put read values 51 | */ 52 | void readValues(READ sourceContext, Predicate keyPredicate, Map readValues); 53 | 54 | /** 55 | * Read a single value from the source context. 56 | * @param sourceContext the context to read from; the context type should be 57 | * {@link Class#isAssignableFrom(Class) assignable} from the type returned by 58 | * {@link #readableType()}. 59 | * @param key the key to use to look up the context value 60 | * @return the value, if present 61 | */ 62 | @Nullable 63 | T readValue(READ sourceContext, Object key); 64 | 65 | /** 66 | * {@link Class} representing the type of context this accessor can restore values to. 67 | */ 68 | Class writeableType(); 69 | 70 | /** 71 | * Write values from a {@link Map} to a target context. 72 | * @param valuesToWrite the values to write to the target context. 73 | * @param targetContext the context to write to; the context type should be 74 | * {@link Class#isAssignableFrom(Class) assignable} from the type returned by 75 | * {@link #writeableType()}. 76 | * @return a context with the written values 77 | */ 78 | WRITE writeValues(Map valuesToWrite, WRITE targetContext); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ContextExecutorService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Collection; 19 | import java.util.List; 20 | import java.util.concurrent.Callable; 21 | import java.util.concurrent.ExecutionException; 22 | import java.util.concurrent.ExecutorService; 23 | import java.util.concurrent.Future; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.TimeoutException; 26 | import java.util.function.Supplier; 27 | import java.util.stream.Collectors; 28 | 29 | /** 30 | * Wraps an {@code ExecutorService} in order to capture context via 31 | * {@link ContextSnapshot} when a task is submitted, and propagate context to the task 32 | * when it is executed. 33 | * 34 | * @author Marcin Grzejszczak 35 | * @author Rossen Stoyanchev 36 | * @since 1.0.0 37 | */ 38 | public class ContextExecutorService implements ExecutorService { 39 | 40 | private final EXECUTOR executorService; 41 | 42 | private final Supplier contextSnapshot; 43 | 44 | /** 45 | * Create an instance of {@link ContextScheduledExecutorService}. 46 | * @param executorService the {@code ExecutorService} to delegate to 47 | * @param contextSnapshot supplier of the {@link ContextSnapshot} - instruction on who 48 | * to retrieve {@link ContextSnapshot} when tasks are scheduled 49 | */ 50 | protected ContextExecutorService(EXECUTOR executorService, Supplier contextSnapshot) { 51 | this.executorService = executorService; 52 | this.contextSnapshot = contextSnapshot; 53 | } 54 | 55 | protected EXECUTOR getExecutorService() { 56 | return this.executorService; 57 | } 58 | 59 | @Override 60 | public Future submit(Callable task) { 61 | return this.executorService.submit(capture().wrap(task)); 62 | } 63 | 64 | @Override 65 | public Future submit(Runnable task, T result) { 66 | return this.executorService.submit(capture().wrap(task), result); 67 | } 68 | 69 | @Override 70 | public Future submit(Runnable task) { 71 | return this.executorService.submit(capture().wrap(task)); 72 | } 73 | 74 | @Override 75 | public List> invokeAll(Collection> tasks) throws InterruptedException { 76 | 77 | List> instrumentedTasks = tasks.stream().map(capture()::wrap).collect(Collectors.toList()); 78 | 79 | return this.executorService.invokeAll(instrumentedTasks); 80 | } 81 | 82 | @Override 83 | public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) 84 | throws InterruptedException { 85 | 86 | List> instrumentedTasks = tasks.stream().map(capture()::wrap).collect(Collectors.toList()); 87 | 88 | return this.executorService.invokeAll(instrumentedTasks, timeout, unit); 89 | } 90 | 91 | @Override 92 | public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { 93 | 94 | List> instrumentedTasks = tasks.stream().map(capture()::wrap).collect(Collectors.toList()); 95 | 96 | return this.executorService.invokeAny(instrumentedTasks); 97 | } 98 | 99 | @Override 100 | public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) 101 | throws InterruptedException, ExecutionException, TimeoutException { 102 | 103 | List> instrumentedTasks = tasks.stream().map(capture()::wrap).collect(Collectors.toList()); 104 | 105 | return this.executorService.invokeAny(instrumentedTasks, timeout, unit); 106 | } 107 | 108 | @Override 109 | public void execute(Runnable command) { 110 | this.executorService.execute(capture().wrap(command)); 111 | } 112 | 113 | @Override 114 | public boolean isShutdown() { 115 | return this.executorService.isShutdown(); 116 | } 117 | 118 | @Override 119 | public boolean isTerminated() { 120 | return this.executorService.isTerminated(); 121 | } 122 | 123 | @Override 124 | public void shutdown() { 125 | this.executorService.shutdown(); 126 | } 127 | 128 | @Override 129 | public List shutdownNow() { 130 | return this.executorService.shutdownNow(); 131 | } 132 | 133 | @Override 134 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 135 | return this.executorService.awaitTermination(timeout, unit); 136 | } 137 | 138 | protected ContextSnapshot capture() { 139 | return this.contextSnapshot.get(); 140 | } 141 | 142 | /** 143 | * Wrap the given {@code ExecutorService} in order to propagate context to any 144 | * executed task through the given {@link ContextSnapshotFactory}. 145 | *

146 | * This method only captures ThreadLocal value. To work with other types of contexts, 147 | * use {@link #wrap(ExecutorService, Supplier)}. 148 | *

149 | * @param service the executorService to wrap 150 | * @param contextSnapshotFactory {@link ContextSnapshotFactory} for capturing a 151 | * {@link ContextSnapshot} at the point when tasks are scheduled 152 | * @return {@code ExecutorService} wrapper 153 | * @since 1.1.2 154 | */ 155 | public static ExecutorService wrap(ExecutorService service, ContextSnapshotFactory contextSnapshotFactory) { 156 | return new ContextExecutorService<>(service, contextSnapshotFactory::captureAll); 157 | } 158 | 159 | /** 160 | * Wrap the given {@code ExecutorService} in order to propagate context to any 161 | * executed task through the given {@link ContextSnapshot} supplier. 162 | *

163 | * Typically, a {@link ContextSnapshotFactory} can be used to supply the snapshot. In 164 | * the case that only ThreadLocal values are to be captured, the 165 | * {@link #wrap(ExecutorService, ContextSnapshotFactory)} variant can be used. 166 | *

167 | * @param service the executorService to wrap 168 | * @param snapshotSupplier supplier for capturing a {@link ContextSnapshot} at the 169 | * point when tasks are scheduled 170 | * @return {@code ExecutorService} wrapper 171 | */ 172 | public static ExecutorService wrap(ExecutorService service, Supplier snapshotSupplier) { 173 | return new ContextExecutorService<>(service, snapshotSupplier); 174 | } 175 | 176 | /** 177 | * Variant of {@link #wrap(ExecutorService, Supplier)} that uses 178 | * {@link ContextSnapshot#captureAll(Object...)} to create the context snapshot. 179 | * @param service the executorService to wrap 180 | * @return {@code ExecutorService} wrapper 181 | * @deprecated use {@link #wrap(ExecutorService, Supplier)} 182 | */ 183 | @Deprecated 184 | public static ExecutorService wrap(ExecutorService service) { 185 | return wrap(service, 186 | () -> DefaultContextSnapshotFactory.captureAll(ContextRegistry.getInstance(), key -> true, false)); 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ContextRegistry.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.ServiceLoader; 21 | import java.util.concurrent.CopyOnWriteArrayList; 22 | import java.util.function.Consumer; 23 | import java.util.function.Supplier; 24 | 25 | /** 26 | * Registry that provides access to, instances of {@link ContextAccessor} and 27 | * {@link ThreadLocalAccessor}. 28 | * 29 | *

30 | * A static instance is available via {@link #getInstance()}. It is intended to be 31 | * initialized on startup, and to be aware of all available accessors, as many as 32 | * possible. The means to control what context gets propagated is in 33 | * {@link ContextSnapshot}, which filters context values by key. 34 | * 35 | * @author Rossen Stoyanchev 36 | * @since 1.0.0 37 | */ 38 | public class ContextRegistry { 39 | 40 | private static final ContextRegistry instance = new ContextRegistry().loadContextAccessors() 41 | .loadThreadLocalAccessors(); 42 | 43 | private final List> contextAccessors = new CopyOnWriteArrayList<>(); 44 | 45 | private final List> threadLocalAccessors = new CopyOnWriteArrayList<>(); 46 | 47 | private final List> readOnlyContextAccessors = Collections 48 | .unmodifiableList(this.contextAccessors); 49 | 50 | private final List> readOnlyThreadLocalAccessors = Collections 51 | .unmodifiableList(this.threadLocalAccessors); 52 | 53 | /** 54 | * Register a {@link ContextAccessor}. If there is an existing registration of another 55 | * {@code ContextAccessor} that can work with its declared types, an exception is 56 | * thrown. 57 | */ 58 | public ContextRegistry registerContextAccessor(ContextAccessor accessor) { 59 | for (ContextAccessor existing : this.contextAccessors) { 60 | if (existing.readableType().isAssignableFrom(accessor.readableType()) 61 | || accessor.readableType().isAssignableFrom(existing.readableType())) { 62 | throw new IllegalArgumentException( 63 | "Found an already registered accessor (" + existing.getClass().getCanonicalName() + ") reading " 64 | + existing.readableType().getCanonicalName() + " when trying to add accessor (" 65 | + accessor.getClass().getCanonicalName() + ") reading " 66 | + accessor.readableType().getCanonicalName()); 67 | } 68 | if (existing.writeableType().isAssignableFrom(accessor.writeableType()) 69 | || accessor.writeableType().isAssignableFrom(existing.writeableType())) { 70 | throw new IllegalArgumentException( 71 | "Found an already registered accessor (" + existing.getClass().getCanonicalName() + ") writing " 72 | + existing.writeableType().getCanonicalName() + " when trying to add accessor (" 73 | + accessor.getClass().getCanonicalName() + ") writing " 74 | + accessor.writeableType().getCanonicalName()); 75 | } 76 | } 77 | this.contextAccessors.add(accessor); 78 | return this; 79 | } 80 | 81 | /** 82 | * Register a {@link ThreadLocalAccessor} for the given {@link ThreadLocal}. 83 | * @param key the {@link ThreadLocalAccessor#key() key} to associate with the 84 | * ThreadLocal value 85 | * @param threadLocal the underlying {@code ThreadLocal} 86 | * @return the same registry instance 87 | * @param the type of value stored in the ThreadLocal 88 | */ 89 | public ContextRegistry registerThreadLocalAccessor(String key, ThreadLocal threadLocal) { 90 | return registerThreadLocalAccessor(key, threadLocal::get, threadLocal::set, threadLocal::remove); 91 | } 92 | 93 | /** 94 | * Register a {@link ThreadLocalAccessor} from callbacks. 95 | * @param key the {@link ThreadLocalAccessor#key() key} to associate with the 96 | * ThreadLocal value 97 | * @param getSupplier callback to use for getting the value 98 | * @param setConsumer callback to use for setting the value 99 | * @param resetTask callback to use for resetting the value 100 | * @return the same registry instance 101 | * @param the type of value stored in the ThreadLocal 102 | */ 103 | public ContextRegistry registerThreadLocalAccessor(String key, Supplier getSupplier, Consumer setConsumer, 104 | Runnable resetTask) { 105 | 106 | return registerThreadLocalAccessor(new ThreadLocalAccessor() { 107 | 108 | @Override 109 | public Object key() { 110 | return key; 111 | } 112 | 113 | @Nullable 114 | @Override 115 | public V getValue() { 116 | return getSupplier.get(); 117 | } 118 | 119 | @Override 120 | public void setValue(V value) { 121 | setConsumer.accept(value); 122 | } 123 | 124 | @Override 125 | public void setValue() { 126 | resetTask.run(); 127 | } 128 | }); 129 | } 130 | 131 | /** 132 | * Register a {@link ThreadLocalAccessor}. If there is an existing registration with 133 | * the same {@link ThreadLocalAccessor#key() key}, it is removed first. 134 | * @param accessor the accessor to register 135 | * @return the same registry instance 136 | */ 137 | public ContextRegistry registerThreadLocalAccessor(ThreadLocalAccessor accessor) { 138 | for (ThreadLocalAccessor existing : this.threadLocalAccessors) { 139 | if (existing.key().equals(accessor.key())) { 140 | this.threadLocalAccessors.remove(existing); 141 | break; 142 | } 143 | } 144 | this.threadLocalAccessors.add(accessor); 145 | return this; 146 | } 147 | 148 | /** 149 | * Removes a {@link ThreadLocalAccessor}. 150 | * @param key under which the accessor got registered 151 | * @return {@code true} when accessor got successfully removed 152 | */ 153 | public boolean removeThreadLocalAccessor(String key) { 154 | for (ThreadLocalAccessor existing : this.threadLocalAccessors) { 155 | if (existing.key().equals(key)) { 156 | return this.threadLocalAccessors.remove(existing); 157 | } 158 | } 159 | return false; 160 | } 161 | 162 | /** 163 | * Removes a registered {@link ContextAccessor}. 164 | * @param accessorToRemove accessor instance to remove 165 | * @return {@code true} when accessor got successfully removed 166 | */ 167 | public boolean removeContextAccessor(ContextAccessor accessorToRemove) { 168 | return this.contextAccessors.remove(accessorToRemove); 169 | } 170 | 171 | /** 172 | * Load {@link ContextAccessor} implementations through the {@link ServiceLoader} 173 | * mechanism. 174 | *

175 | * Note that existing registrations of the same {@code ContextAccessor} type, if any, 176 | * are removed first. 177 | */ 178 | public ContextRegistry loadContextAccessors() { 179 | ServiceLoader.load(ContextAccessor.class).forEach(this::registerContextAccessor); 180 | return this; 181 | } 182 | 183 | /** 184 | * Load {@link ThreadLocalAccessor} implementations through the {@link ServiceLoader} 185 | * mechanism. 186 | *

187 | * Note that existing registrations with the same {@link ThreadLocalAccessor#key() 188 | * key}, if any, are removed first. 189 | */ 190 | public ContextRegistry loadThreadLocalAccessors() { 191 | ServiceLoader.load(ThreadLocalAccessor.class).forEach(this::registerThreadLocalAccessor); 192 | return this; 193 | } 194 | 195 | /** 196 | * Find a {@link ContextAccessor} that can read the given context. 197 | * @param context the context to read from 198 | * @throws IllegalStateException if no match is found 199 | */ 200 | public ContextAccessor getContextAccessorForRead(Object context) { 201 | for (ContextAccessor accessor : this.contextAccessors) { 202 | if (accessor.readableType().isAssignableFrom(context.getClass())) { 203 | return accessor; 204 | } 205 | } 206 | throw new IllegalStateException("No ContextAccessor for contextType: " + context.getClass()); 207 | } 208 | 209 | /** 210 | * Return a {@link ContextAccessor} that can write the given context. 211 | * @param context the context to write to 212 | * @throws IllegalStateException if no match is found 213 | */ 214 | public ContextAccessor getContextAccessorForWrite(Object context) { 215 | for (ContextAccessor accessor : this.contextAccessors) { 216 | if (accessor.writeableType().isAssignableFrom(context.getClass())) { 217 | return accessor; 218 | } 219 | } 220 | throw new IllegalStateException("No ContextAccessor for contextType: " + context.getClass()); 221 | } 222 | 223 | /** 224 | * Return a read-only list of registered {@link ContextAccessor}'s. 225 | */ 226 | public List> getContextAccessors() { 227 | return this.readOnlyContextAccessors; 228 | } 229 | 230 | /** 231 | * Return a read-only list of registered {@link ThreadLocalAccessor}'s. 232 | */ 233 | public List> getThreadLocalAccessors() { 234 | return this.readOnlyThreadLocalAccessors; 235 | } 236 | 237 | @Override 238 | public String toString() { 239 | return "ContextRegistry{" + "contextAccessors=" + this.contextAccessors + ", " + "threadLocalAccessors=" 240 | + this.threadLocalAccessors + "}"; 241 | } 242 | 243 | /** 244 | * Return a global {@link ContextRegistry} instance. 245 | *

246 | * Note: The global instance should be initialized on startup to 247 | * ensure it has the ability to propagate to and from different types of context 248 | * throughout the application. The registry itself is not intended to as a mechanism 249 | * to control what gets propagated. It is in {@link ContextSnapshot} where more 250 | * fine-grained decisions can be made about which context values to propagate. 251 | */ 252 | public static ContextRegistry getInstance() { 253 | return instance; 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ContextScheduledExecutorService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.concurrent.Callable; 19 | import java.util.concurrent.ScheduledExecutorService; 20 | import java.util.concurrent.ScheduledFuture; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.function.Supplier; 23 | 24 | /** 25 | * Wraps a {@code ScheduledExecutorService} in order to capture context via 26 | * {@link ContextSnapshot} when a task is submitted, and propagate context to the task 27 | * when it is executed. 28 | * 29 | * @author Marcin Grzejszczak 30 | * @author Rossen Stoyanchev 31 | * @since 1.0.0 32 | */ 33 | public final class ContextScheduledExecutorService extends ContextExecutorService 34 | implements ScheduledExecutorService { 35 | 36 | /** 37 | * Create an instance 38 | * @param service the {@code ScheduledExecutorService} to delegate to 39 | * @param snapshotSupplier supplier of the {@link ContextSnapshot} - instruction on 40 | * who to retrieve {@link ContextSnapshot} when tasks are scheduled 41 | */ 42 | private ContextScheduledExecutorService(ScheduledExecutorService service, 43 | Supplier snapshotSupplier) { 44 | super(service, snapshotSupplier); 45 | } 46 | 47 | @Override 48 | public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { 49 | return getExecutorService().schedule(capture().wrap(command), delay, unit); 50 | } 51 | 52 | @Override 53 | public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { 54 | return getExecutorService().schedule(capture().wrap(callable), delay, unit); 55 | } 56 | 57 | @Override 58 | public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { 59 | return getExecutorService().scheduleAtFixedRate(capture().wrap(command), initialDelay, period, unit); 60 | } 61 | 62 | @Override 63 | public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { 64 | return getExecutorService().scheduleWithFixedDelay(capture().wrap(command), initialDelay, delay, unit); 65 | } 66 | 67 | /** 68 | * Wrap the given {@code ScheduledExecutorService} in order to propagate context to 69 | * any executed task through the given {@link ContextSnapshotFactory}. 70 | *

71 | * This method only captures ThreadLocal value. To work with other types of contexts, 72 | * use {@link #wrap(ScheduledExecutorService, Supplier)}. 73 | *

74 | * @param service the executorService to wrap 75 | * @param contextSnapshotFactory {@link ContextSnapshotFactory} for capturing a 76 | * {@link ContextSnapshot} at the point when tasks are scheduled 77 | * @return {@code ScheduledExecutorService} wrapper 78 | * @since 1.1.2 79 | */ 80 | public static ScheduledExecutorService wrap(ScheduledExecutorService service, 81 | ContextSnapshotFactory contextSnapshotFactory) { 82 | return new ContextScheduledExecutorService(service, contextSnapshotFactory::captureAll); 83 | } 84 | 85 | /** 86 | * Wrap the given {@code ScheduledExecutorService} in order to propagate context to 87 | * any executed task through the given {@link ContextSnapshot} supplier. 88 | *

89 | * Typically, a {@link ContextSnapshotFactory} can be used to supply the snapshot. In 90 | * the case that only ThreadLocal values are to be captured, the 91 | * {@link #wrap(ScheduledExecutorService, ContextSnapshotFactory)} variant can be 92 | * used. 93 | *

94 | * @param service the executorService to wrap 95 | * @param supplier supplier for capturing a {@link ContextSnapshot} at the point when 96 | * tasks are scheduled 97 | * @return {@code ScheduledExecutorService} wrapper 98 | */ 99 | public static ScheduledExecutorService wrap(ScheduledExecutorService service, Supplier supplier) { 100 | return new ContextScheduledExecutorService(service, supplier); 101 | } 102 | 103 | /** 104 | * Variant of {@link #wrap(ScheduledExecutorService, Supplier)} that uses 105 | * {@link ContextSnapshot#captureAll(Object...)} to create the context snapshot. 106 | * @param service the executorService to wrap 107 | * @return {@code ScheduledExecutorService} wrapper 108 | * @deprecated use {@link #wrap(ScheduledExecutorService, Supplier)} 109 | */ 110 | @Deprecated 111 | public static ScheduledExecutorService wrap(ScheduledExecutorService service) { 112 | return wrap(service, 113 | () -> DefaultContextSnapshotFactory.captureAll(ContextRegistry.getInstance(), key -> true, false)); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ContextSnapshotFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.function.Predicate; 19 | 20 | /** 21 | * Factory for creating {@link ContextSnapshot} objects and restoring {@link ThreadLocal} 22 | * values using a context object for which a {@link ContextAccessor} exists in the 23 | * {@link ContextRegistry}. 24 | * 25 | * @author Dariusz Jędrzejczyk 26 | * @since 1.0.3 27 | */ 28 | public interface ContextSnapshotFactory { 29 | 30 | /** 31 | * Capture values from {@link ThreadLocal} and from other context objects using all 32 | * accessors from a {@link ContextRegistry} instance. 33 | *

34 | * Values captured multiple times are overridden in the snapshot by the order of 35 | * contexts given as arguments. 36 | * @param contexts context objects to extract values from 37 | * @return a snapshot with saved context values 38 | */ 39 | ContextSnapshot captureAll(Object... contexts); 40 | 41 | /** 42 | * Create a {@link ContextSnapshot} by reading values from the given context objects. 43 | *

44 | * Values captured multiple times are overridden in the snapshot by the order of 45 | * contexts given as arguments. 46 | * @param contexts the contexts to read values from 47 | * @return the created {@link ContextSnapshot} 48 | */ 49 | ContextSnapshot captureFrom(Object... contexts); 50 | 51 | /** 52 | * Read the values specified by keys from the given source context, and if found, use 53 | * them to set {@link ThreadLocal} values. If no keys are provided, all keys are used. 54 | * Essentially, a shortcut that bypasses the need to create of {@link ContextSnapshot} 55 | * first via {@link #captureFrom(Object...)}, followed by 56 | * {@link ContextSnapshot#setThreadLocals()}. 57 | * @param sourceContext the source context to read values from 58 | * @param keys the keys of the values to read. If none provided, all keys are 59 | * considered. 60 | * @param the type of the target context 61 | * @return an object that can be used to reset {@link ThreadLocal} values at the end 62 | * of the context scope, either removing them or restoring their previous values, if 63 | * any. 64 | */ 65 | ContextSnapshot.Scope setThreadLocalsFrom(Object sourceContext, String... keys); 66 | 67 | /** 68 | * Creates a builder for configuring the factory. 69 | * @return an instance that provides defaults, that can be configured to provide to 70 | * the created {@link ContextSnapshotFactory}. 71 | */ 72 | static Builder builder() { 73 | return new DefaultContextSnapshotFactory.Builder(); 74 | } 75 | 76 | /** 77 | * Builder for {@link ContextSnapshotFactory} instances. 78 | */ 79 | interface Builder { 80 | 81 | /** 82 | * Creates a new instance of {@link ContextSnapshotFactory}. 83 | * @return an instance configured by the values set on the builder 84 | */ 85 | ContextSnapshotFactory build(); 86 | 87 | /** 88 | * Determines whether to clear existing {@link ThreadLocal} values at the start of 89 | * a scope, if there are no corresponding values in the source 90 | * {@link ContextSnapshot} or context object. 91 | * @param shouldClear if {@code true}, values not present in the context object or 92 | * snapshot will be cleared at the start of a scope and later restored 93 | * @return this builder instance 94 | */ 95 | Builder clearMissing(boolean shouldClear); 96 | 97 | /** 98 | * Configures the {@link ContextRegistry} to use by the created factory. 99 | * @param contextRegistry the registry to use 100 | * @return this builder instance 101 | */ 102 | Builder contextRegistry(ContextRegistry contextRegistry); 103 | 104 | /** 105 | * Instructs the factory to use the given predicate to select matching keys when 106 | * capturing {@link ThreadLocal} values 107 | * @param captureKeyPredicate predicate used to select matching keys 108 | * @return this builder instance 109 | */ 110 | Builder captureKeyPredicate(Predicate captureKeyPredicate); 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/DefaultContextSnapshot.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.function.Predicate; 22 | 23 | /** 24 | * Default implementation of {@link ContextSnapshot}. 25 | * 26 | * @author Rossen Stoyanchev 27 | * @author Brian Clozel 28 | * @author Dariusz Jędrzejczyk 29 | * @since 1.0.0 30 | */ 31 | final class DefaultContextSnapshot extends HashMap implements ContextSnapshot { 32 | 33 | private final ContextRegistry contextRegistry; 34 | 35 | private final boolean clearMissing; 36 | 37 | DefaultContextSnapshot(ContextRegistry contextRegistry, boolean clearMissing) { 38 | this.contextRegistry = contextRegistry; 39 | this.clearMissing = clearMissing; 40 | } 41 | 42 | @Override 43 | public C updateContext(C context) { 44 | return updateContextInternal(context, this); 45 | } 46 | 47 | @Override 48 | public C updateContext(C context, Predicate keyPredicate) { 49 | if (!isEmpty()) { 50 | Map valuesToWrite = new HashMap<>(); 51 | this.forEach((key, value) -> { 52 | if (keyPredicate.test(key)) { 53 | valuesToWrite.put(key, value); 54 | } 55 | }); 56 | context = updateContextInternal(context, valuesToWrite); 57 | } 58 | return context; 59 | } 60 | 61 | @SuppressWarnings("unchecked") 62 | private C updateContextInternal(C context, Map valueContainer) { 63 | if (!isEmpty()) { 64 | ContextAccessor accessor = this.contextRegistry.getContextAccessorForWrite(context); 65 | context = ((ContextAccessor) accessor).writeValues(valueContainer, context); 66 | } 67 | return context; 68 | } 69 | 70 | @Override 71 | public Scope setThreadLocals() { 72 | return setThreadLocals(key -> true); 73 | } 74 | 75 | @Override 76 | public Scope setThreadLocals(Predicate keyPredicate) { 77 | Map previousValues = null; 78 | List> accessors = this.contextRegistry.getThreadLocalAccessors(); 79 | for (int i = 0; i < accessors.size(); ++i) { 80 | ThreadLocalAccessor accessor = accessors.get(i); 81 | Object key = accessor.key(); 82 | if (keyPredicate.test(key)) { 83 | if (this.containsKey(key)) { 84 | Object value = get(key); 85 | assert value != null : "snapshot contains disallowed null mapping for key: " + key; 86 | previousValues = setThreadLocal(key, value, accessor, previousValues); 87 | } 88 | else if (clearMissing) { 89 | previousValues = clearThreadLocal(key, accessor, previousValues); 90 | } 91 | } 92 | } 93 | return DefaultScope.from(previousValues, this.contextRegistry); 94 | } 95 | 96 | @SuppressWarnings("unchecked") 97 | static Map setThreadLocal(Object key, V value, ThreadLocalAccessor accessor, 98 | @Nullable Map previousValues) { 99 | 100 | previousValues = (previousValues != null ? previousValues : new HashMap<>()); 101 | previousValues.put(key, accessor.getValue()); 102 | ((ThreadLocalAccessor) accessor).setValue(value); 103 | return previousValues; 104 | } 105 | 106 | @SuppressWarnings("unchecked") 107 | static Map clearThreadLocal(Object key, ThreadLocalAccessor accessor, 108 | @Nullable Map previousValues) { 109 | previousValues = (previousValues != null ? previousValues : new HashMap<>()); 110 | previousValues.put(key, accessor.getValue()); 111 | accessor.setValue(); 112 | return previousValues; 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return "DefaultContextSnapshot" + super.toString(); 118 | } 119 | 120 | /** 121 | * Default implementation of {@link Scope}. 122 | */ 123 | static class DefaultScope implements Scope { 124 | 125 | private final Map previousValues; 126 | 127 | private final ContextRegistry contextRegistry; 128 | 129 | private DefaultScope(Map previousValues, ContextRegistry contextRegistry) { 130 | this.previousValues = previousValues; 131 | this.contextRegistry = contextRegistry; 132 | } 133 | 134 | @Override 135 | public void close() { 136 | List> accessors = this.contextRegistry.getThreadLocalAccessors(); 137 | for (int i = accessors.size() - 1; i >= 0; --i) { 138 | ThreadLocalAccessor accessor = accessors.get(i); 139 | if (this.previousValues.containsKey(accessor.key())) { 140 | Object previousValue = this.previousValues.get(accessor.key()); 141 | resetThreadLocalValue(accessor, previousValue); 142 | } 143 | } 144 | } 145 | 146 | @SuppressWarnings("unchecked") 147 | private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullable V previousValue) { 148 | if (previousValue != null) { 149 | ((ThreadLocalAccessor) accessor).restore(previousValue); 150 | } 151 | else { 152 | accessor.restore(); 153 | } 154 | } 155 | 156 | public static Scope from(@Nullable Map previousValues, ContextRegistry registry) { 157 | return (previousValues != null ? new DefaultScope(previousValues, registry) : () -> { 158 | }); 159 | } 160 | 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/DefaultContextSnapshotFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Objects; 21 | import java.util.function.Predicate; 22 | 23 | /** 24 | * Default implementation of {@link ContextSnapshotFactory}. 25 | * 26 | * @author Dariusz Jędrzejczyk 27 | * @since 1.0.3 28 | */ 29 | final class DefaultContextSnapshotFactory implements ContextSnapshotFactory { 30 | 31 | private static final DefaultContextSnapshot emptyContextSnapshot = new DefaultContextSnapshot(new ContextRegistry(), 32 | false); 33 | 34 | private final ContextRegistry contextRegistry; 35 | 36 | private final boolean clearMissing; 37 | 38 | private final Predicate captureKeyPredicate; 39 | 40 | public DefaultContextSnapshotFactory(ContextRegistry contextRegistry, boolean clearMissing, 41 | Predicate captureKeyPredicate) { 42 | this.contextRegistry = contextRegistry; 43 | this.clearMissing = clearMissing; 44 | this.captureKeyPredicate = captureKeyPredicate; 45 | } 46 | 47 | private static DefaultContextSnapshot clearingEmptySnapshot(ContextRegistry contextRegistry) { 48 | return new DefaultContextSnapshot(contextRegistry, true); 49 | } 50 | 51 | @Override 52 | public ContextSnapshot captureAll(Object... contexts) { 53 | return captureAll(contextRegistry, captureKeyPredicate, clearMissing, contexts); 54 | } 55 | 56 | static ContextSnapshot captureAll(ContextRegistry contextRegistry, Predicate keyPredicate, 57 | boolean clearMissing, Object... contexts) { 58 | 59 | DefaultContextSnapshot snapshot = captureFromThreadLocals(keyPredicate, clearMissing, contextRegistry); 60 | for (Object context : contexts) { 61 | snapshot = captureFromContext(keyPredicate, clearMissing, contextRegistry, snapshot, context); 62 | } 63 | return (snapshot != null ? snapshot 64 | : (clearMissing ? clearingEmptySnapshot(contextRegistry) : emptyContextSnapshot)); 65 | } 66 | 67 | @Nullable 68 | private static DefaultContextSnapshot captureFromThreadLocals(Predicate keyPredicate, boolean clearMissing, 69 | ContextRegistry contextRegistry) { 70 | 71 | DefaultContextSnapshot snapshot = null; 72 | for (ThreadLocalAccessor accessor : contextRegistry.getThreadLocalAccessors()) { 73 | if (keyPredicate.test(accessor.key())) { 74 | Object value = accessor.getValue(); 75 | if (value != null) { 76 | snapshot = (snapshot != null ? snapshot 77 | : new DefaultContextSnapshot(contextRegistry, clearMissing)); 78 | snapshot.put(accessor.key(), value); 79 | } 80 | } 81 | } 82 | return snapshot; 83 | } 84 | 85 | @Override 86 | public ContextSnapshot captureFrom(Object... contexts) { 87 | return captureFromContext(captureKeyPredicate, clearMissing, contextRegistry, null, contexts); 88 | } 89 | 90 | @SuppressWarnings("unchecked") 91 | static DefaultContextSnapshot captureFromContext(Predicate keyPredicate, boolean clearMissing, 92 | ContextRegistry contextRegistry, @Nullable DefaultContextSnapshot snapshot, Object... contexts) { 93 | 94 | for (Object context : contexts) { 95 | ContextAccessor accessor = contextRegistry.getContextAccessorForRead(context); 96 | snapshot = (snapshot != null ? snapshot : new DefaultContextSnapshot(contextRegistry, clearMissing)); 97 | ((ContextAccessor) accessor).readValues(context, keyPredicate, snapshot); 98 | } 99 | if (snapshot != null) { 100 | snapshot.values().removeIf(Objects::isNull); 101 | } 102 | return (snapshot != null ? snapshot 103 | : (clearMissing ? clearingEmptySnapshot(contextRegistry) : emptyContextSnapshot)); 104 | } 105 | 106 | @Override 107 | public ContextSnapshot.Scope setThreadLocalsFrom(Object sourceContext, String... keys) { 108 | if (keys == null || keys.length == 0) { 109 | return setAllThreadLocalsFrom(sourceContext, contextRegistry, clearMissing); 110 | } 111 | else { 112 | return setThreadLocalsFrom(sourceContext, contextRegistry, clearMissing, keys); 113 | } 114 | } 115 | 116 | @SuppressWarnings("unchecked") 117 | static ContextSnapshot.Scope setAllThreadLocalsFrom(Object sourceContext, ContextRegistry contextRegistry, 118 | boolean clearMissing) { 119 | ContextAccessor contextAccessor = contextRegistry.getContextAccessorForRead(sourceContext); 120 | Map previousValues = null; 121 | for (ThreadLocalAccessor threadLocalAccessor : contextRegistry.getThreadLocalAccessors()) { 122 | Object key = threadLocalAccessor.key(); 123 | Object value = ((ContextAccessor) contextAccessor).readValue((C) sourceContext, key); 124 | if (value != null) { 125 | previousValues = DefaultContextSnapshot.setThreadLocal(key, value, threadLocalAccessor, previousValues); 126 | } 127 | else if (clearMissing) { 128 | previousValues = DefaultContextSnapshot.clearThreadLocal(key, threadLocalAccessor, previousValues); 129 | } 130 | } 131 | return DefaultContextSnapshot.DefaultScope.from(previousValues, contextRegistry); 132 | } 133 | 134 | @SuppressWarnings("unchecked") 135 | static ContextSnapshot.Scope setThreadLocalsFrom(Object sourceContext, ContextRegistry contextRegistry, 136 | boolean clearMissing, String... keys) { 137 | if (keys == null || keys.length == 0) { 138 | throw new IllegalArgumentException("You must provide at least one key when setting thread locals"); 139 | } 140 | ContextAccessor contextAccessor = contextRegistry.getContextAccessorForRead(sourceContext); 141 | Map previousValues = null; 142 | List> accessors = contextRegistry.getThreadLocalAccessors(); 143 | for (String key : keys) { 144 | Object value = ((ContextAccessor) contextAccessor).readValue((C) sourceContext, key); 145 | if (value != null) { 146 | for (ThreadLocalAccessor threadLocalAccessor : accessors) { 147 | if (key.equals(threadLocalAccessor.key())) { 148 | previousValues = DefaultContextSnapshot.setThreadLocal(key, value, threadLocalAccessor, 149 | previousValues); 150 | break; 151 | } 152 | } 153 | } 154 | else if (clearMissing) { 155 | for (ThreadLocalAccessor threadLocalAccessor : accessors) { 156 | if (key.equals(threadLocalAccessor.key())) { 157 | previousValues = DefaultContextSnapshot.clearThreadLocal(key, threadLocalAccessor, 158 | previousValues); 159 | break; 160 | } 161 | } 162 | } 163 | } 164 | return DefaultContextSnapshot.DefaultScope.from(previousValues, contextRegistry); 165 | } 166 | 167 | static final class Builder implements ContextSnapshotFactory.Builder { 168 | 169 | private boolean clearMissing = false; 170 | 171 | private ContextRegistry contextRegistry = ContextRegistry.getInstance(); 172 | 173 | private Predicate captureKeyPredicate = key -> true; 174 | 175 | public Builder() { 176 | } 177 | 178 | @Override 179 | public ContextSnapshotFactory build() { 180 | return new DefaultContextSnapshotFactory(contextRegistry, clearMissing, captureKeyPredicate); 181 | } 182 | 183 | @Override 184 | public ContextSnapshotFactory.Builder clearMissing(boolean shouldClear) { 185 | this.clearMissing = shouldClear; 186 | return this; 187 | } 188 | 189 | @Override 190 | public ContextSnapshotFactory.Builder contextRegistry(ContextRegistry contextRegistry) { 191 | this.contextRegistry = contextRegistry; 192 | return this; 193 | } 194 | 195 | @Override 196 | public ContextSnapshotFactory.Builder captureKeyPredicate(Predicate captureKeyPredicate) { 197 | this.captureKeyPredicate = captureKeyPredicate; 198 | return this; 199 | } 200 | 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/NonNullApi.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | import javax.annotation.Nonnull; 25 | import javax.annotation.meta.TypeQualifierDefault; 26 | 27 | /** 28 | * Declares all method parameters and return values within a package as non-nullable via 29 | * JSR-305 meta-annotations to indicate nullability in Java. 30 | * 31 | * @author Rossen Stoyanchev 32 | * @since 1.0.0 33 | */ 34 | @Target(ElementType.PACKAGE) 35 | @Retention(RetentionPolicy.RUNTIME) 36 | @Documented 37 | @Nonnull 38 | @TypeQualifierDefault({ ElementType.METHOD, ElementType.PARAMETER }) 39 | public @interface NonNullApi { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/NonNullFields.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | import javax.annotation.Nonnull; 25 | import javax.annotation.meta.TypeQualifierDefault; 26 | 27 | /** 28 | * Declares all fields within a package as non-nullable via JSR-305 meta-annotations to 29 | * indicate nullability in Java. 30 | * 31 | * @author Rossen Stoyanchev 32 | * @since 1.0.0 33 | */ 34 | @Target(ElementType.PACKAGE) 35 | @Retention(RetentionPolicy.RUNTIME) 36 | @Documented 37 | @Nonnull 38 | @TypeQualifierDefault(ElementType.FIELD) 39 | public @interface NonNullFields { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/Nullable.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | import javax.annotation.Nonnull; 25 | import javax.annotation.meta.TypeQualifierNickname; 26 | import javax.annotation.meta.When; 27 | 28 | /** 29 | * Annotation to declare that a method parameter, return value, or field can be 30 | * {@code null} under some circumstance. Uses JSR-305 meta-annotations to indicate 31 | * nullability. Overridden methods override should redeclare the annotation unless they 32 | * behave differently. 33 | * 34 | * @author Rossen Stoyanchev 35 | * @since 1.0.0 36 | * @see NonNullApi 37 | * @see NonNullFields 38 | */ 39 | @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) 40 | @Retention(RetentionPolicy.RUNTIME) 41 | @Documented 42 | @Nonnull(when = When.MAYBE) 43 | @TypeQualifierNickname 44 | public @interface Nullable { 45 | 46 | } 47 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/ThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.function.Consumer; 19 | import java.util.function.Supplier; 20 | 21 | /** 22 | * Contract to assist with setting and clearing a {@link ThreadLocal}. 23 | * 24 | * @author Rossen Stoyanchev 25 | * @author Marcin Grzejszczak 26 | * @author Dariusz Jędrzejczyk 27 | * @since 1.0.0 28 | * @see ContextRegistry#registerThreadLocalAccessor(ThreadLocalAccessor) 29 | * @see ContextRegistry#registerThreadLocalAccessor(String, Supplier, Consumer, Runnable) 30 | */ 31 | public interface ThreadLocalAccessor { 32 | 33 | /** 34 | * The key to associate with the ThreadLocal value when saved within a 35 | * {@link ContextSnapshot}. 36 | */ 37 | Object key(); 38 | 39 | /** 40 | * Return the current {@link ThreadLocal} value. 41 | *

42 | * This method is called in two scenarios: 43 | *

    44 | *
  • When capturing a {@link ContextSnapshot}. A {@code null} value is ignored and 45 | * the {@link #key()} will not be present in the snapshot.
  • 46 | *
  • When setting {@link ThreadLocal} values from a {@link ContextSnapshot} or from 47 | * a Context object (through a {@link ContextAccessor}) to save existing values in 48 | * order to {@link #restore(Object)} them at the end of the scope. A {@code null} 49 | * value means the {@link ThreadLocal} should not be set and upon closing a 50 | * {@link io.micrometer.context.ContextSnapshot.Scope}, the {@link #restore()} variant 51 | * is called.
  • 52 | *
53 | */ 54 | @Nullable 55 | V getValue(); 56 | 57 | /** 58 | * Set the {@link ThreadLocal} at the start of a 59 | * {@link io.micrometer.context.ContextSnapshot.Scope} to a value obtained from a 60 | * {@link ContextSnapshot} or from a different type of context (through a 61 | * {@link ContextAccessor}). 62 | *

63 | * @param value the value to set, never {@code null} when called from a 64 | * {@link ContextSnapshot} implementation, which is not allowed to store mappings to 65 | * {@code null}. 66 | */ 67 | void setValue(V value); 68 | 69 | /** 70 | * Called instead of {@link #setValue(Object)} in order to remove the current 71 | * {@link ThreadLocal} value at the start of a 72 | * {@link io.micrometer.context.ContextSnapshot.Scope}. 73 | * 74 | * @since 1.0.3 75 | */ 76 | default void setValue() { 77 | reset(); 78 | } 79 | 80 | /** 81 | * Remove the {@link ThreadLocal} value when setting {@link ThreadLocal} values in 82 | * case of missing mapping for a {@link #key()} from a {@link ContextSnapshot}, or a 83 | * Context object (operated upon by {@link ContextAccessor}). 84 | * @deprecated To be replaced by calls to {@link #setValue()} (and/or 85 | * {@link #restore()}), which needs to be implemented when this implementation is 86 | * removed. 87 | */ 88 | @Deprecated 89 | default void reset() { 90 | throw new IllegalStateException(this.getClass().getName() + "#reset() should " 91 | + "not be called. Please implement #setValue() method when removing the " + "#reset() implementation."); 92 | } 93 | 94 | /** 95 | * Restore the {@link ThreadLocal} at the end of a 96 | * {@link io.micrometer.context.ContextSnapshot.Scope} to the previous value it had 97 | * before the start of the scope. 98 | *

99 | * @param previousValue previous value to set, never {@code null} when called from a 100 | * {@link ContextSnapshot} * implementation, which is not allowed to store mappings to 101 | * {@code null}. 102 | * @since 1.0.1 103 | */ 104 | default void restore(V previousValue) { 105 | setValue(previousValue); 106 | } 107 | 108 | /** 109 | * Called instead of {@link #restore(Object)} when there was no {@link ThreadLocal} 110 | * value existing at the start of the scope. 111 | * @see #getValue() 112 | * @since 1.0.3 113 | */ 114 | default void restore() { 115 | setValue(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/integration/Slf4jThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context.integration; 17 | 18 | import java.util.Arrays; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | import io.micrometer.context.ThreadLocalAccessor; 24 | import org.slf4j.MDC; 25 | 26 | /** 27 | * {@link ThreadLocalAccessor} for copying contents of the {@link MDC} across 28 | * {@link Thread} boundaries. It can work with selected keys only or copy the entire 29 | * contents of the {@link MDC}. It is recommended to use only when no other 30 | * {@link ThreadLocalAccessor} interacts with the MDC or the selected keys, for 31 | * instance tracing libraries. 32 | * 33 | * @author Dariusz Jędrzejczyk 34 | * @since 1.1.1 35 | */ 36 | public class Slf4jThreadLocalAccessor implements ThreadLocalAccessor> { 37 | 38 | /** 39 | * Key under which this accessor is registered in 40 | * {@link io.micrometer.context.ContextRegistry}. 41 | */ 42 | public static final String KEY = "cp.slf4j"; 43 | 44 | private final ThreadLocalAccessor> delegate; 45 | 46 | /** 47 | * Create an instance of {@link Slf4jThreadLocalAccessor}. 48 | * @param keys selected keys for which values from the {@link MDC} should be 49 | * propagated across {@link Thread} boundaries. If none provided, the entire contents 50 | * of the {@link MDC} are propagated. 51 | */ 52 | public Slf4jThreadLocalAccessor(String... keys) { 53 | this.delegate = keys.length == 0 ? new GlobalMdcThreadLocalAccessor() 54 | : new SelectiveMdcThreadLocalAccessor(Arrays.asList(keys)); 55 | } 56 | 57 | @Override 58 | public Object key() { 59 | return KEY; 60 | } 61 | 62 | @Override 63 | public Map getValue() { 64 | return delegate.getValue(); 65 | } 66 | 67 | @Override 68 | public void setValue(Map value) { 69 | delegate.setValue(value); 70 | } 71 | 72 | @Override 73 | public void setValue() { 74 | delegate.setValue(); 75 | } 76 | 77 | private static final class GlobalMdcThreadLocalAccessor implements ThreadLocalAccessor> { 78 | 79 | @Override 80 | public Object key() { 81 | return KEY; 82 | } 83 | 84 | @Override 85 | public Map getValue() { 86 | return MDC.getCopyOfContextMap(); 87 | } 88 | 89 | @Override 90 | public void setValue(Map value) { 91 | MDC.setContextMap(value); 92 | } 93 | 94 | @Override 95 | public void setValue() { 96 | MDC.clear(); 97 | } 98 | 99 | } 100 | 101 | private static final class SelectiveMdcThreadLocalAccessor implements ThreadLocalAccessor> { 102 | 103 | private final List keys; 104 | 105 | SelectiveMdcThreadLocalAccessor(List keys) { 106 | this.keys = keys; 107 | } 108 | 109 | @Override 110 | public Object key() { 111 | return KEY; 112 | } 113 | 114 | @Override 115 | public Map getValue() { 116 | Map values = new HashMap<>(this.keys.size()); 117 | for (String key : this.keys) { 118 | values.put(key, MDC.get(key)); 119 | } 120 | return values; 121 | } 122 | 123 | @Override 124 | public void setValue(Map value) { 125 | for (String key : this.keys) { 126 | String mdcValue = value.get(key); 127 | if (mdcValue != null) { 128 | MDC.put(key, mdcValue); 129 | } 130 | else { 131 | MDC.remove(key); 132 | } 133 | } 134 | } 135 | 136 | @Override 137 | public void setValue() { 138 | for (String key : keys) { 139 | MDC.remove(key); 140 | } 141 | } 142 | 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/integration/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * This package contains useful integrations with external libraries that can be used 18 | * provided those libraries are present at runtime. 19 | */ 20 | @NonNullApi 21 | @NonNullFields 22 | package io.micrometer.context.integration; 23 | 24 | import io.micrometer.context.NonNullApi; 25 | import io.micrometer.context.NonNullFields; 26 | -------------------------------------------------------------------------------- /context-propagation/src/main/java/io/micrometer/context/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * This package contains abstractions and implementations for context propagation. 18 | *

    19 | *
  • {@link io.micrometer.context.ThreadLocalAccessor} and 20 | * {@link io.micrometer.context.ContextAccessor} allow applications and frameworks to plug 21 | * in support for {@link java.lang.ThreadLocal} and other Map-like types of context such 22 | * as the Project Reactor Contexts. 23 | *
  • {@link io.micrometer.context.ContextRegistry} provides a static instance with 24 | * global access to all known accessors that should be registered on startup. 25 | *
  • {@link io.micrometer.context.ContextSnapshot} uses the {@code ContextRegistry} and 26 | * is used to capture context values, and then propagate them from one type of context to 27 | * another or from one thread to another. 28 | *
29 | */ 30 | @NonNullApi 31 | @NonNullFields 32 | package io.micrometer.context; 33 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/AnotherTestContextAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.function.Predicate; 21 | 22 | class AnotherTestContextAccessor implements ContextAccessor { 23 | 24 | @Override 25 | public Class readableType() { 26 | return Set.class; 27 | } 28 | 29 | @Override 30 | public void readValues(Set sourceContext, Predicate keyPredicate, Map readValues) { 31 | 32 | } 33 | 34 | @Nullable 35 | @Override 36 | public T readValue(Set sourceContext, Object key) { 37 | return null; 38 | } 39 | 40 | @Override 41 | public Class writeableType() { 42 | return Set.class; 43 | } 44 | 45 | @Override 46 | public Set writeValues(Map valuesToWrite, Set targetContext) { 47 | return null; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/ContextRegistryTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.function.Predicate; 21 | 22 | import org.junit.jupiter.api.Test; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 26 | 27 | /** 28 | * Unit tests for {@link ContextRegistry}. 29 | * 30 | * @author Rossen Stoyanchev 31 | */ 32 | class ContextRegistryTests { 33 | 34 | private final ContextRegistry registry = new ContextRegistry(); 35 | 36 | @Test 37 | void should_reject_accessors_reading_and_writing_same_or_child_types() { 38 | TestContextAccessor contextAccessor = new TestContextAccessor(); 39 | TestContextAccessor sameTypeContextAccessor = new TestContextAccessor(); 40 | FixedReadHashMapWriterAccessor childTypeWriterAccessor = new FixedReadHashMapWriterAccessor(); 41 | HashMapReaderFixedWriterAccessor childTypeReaderAccessor = new HashMapReaderFixedWriterAccessor(); 42 | 43 | this.registry.registerContextAccessor(contextAccessor); 44 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 45 | 46 | assertThatExceptionOfType(IllegalArgumentException.class) 47 | .isThrownBy(() -> this.registry.registerContextAccessor(sameTypeContextAccessor)); 48 | 49 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 50 | 51 | assertThatExceptionOfType(IllegalArgumentException.class) 52 | .isThrownBy(() -> this.registry.registerContextAccessor(childTypeWriterAccessor)); 53 | 54 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 55 | 56 | assertThatExceptionOfType(IllegalArgumentException.class) 57 | .isThrownBy(() -> this.registry.registerContextAccessor(childTypeReaderAccessor)); 58 | 59 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 60 | } 61 | 62 | @Test 63 | void should_reject_accessors_reading_child_type_already_read_by_existing() { 64 | TestContextAccessor contextAccessor = new TestContextAccessor(); 65 | HashMapReaderAccessor readChildTypeAccessor = new HashMapReaderAccessor(); 66 | 67 | this.registry.registerContextAccessor(contextAccessor); 68 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 69 | 70 | assertThatExceptionOfType(IllegalArgumentException.class) 71 | .isThrownBy(() -> this.registry.registerContextAccessor(readChildTypeAccessor)); 72 | 73 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 74 | } 75 | 76 | @Test 77 | void should_reject_accessor_reading_parent_type_of_type_read_by_existing() { 78 | HashMapReaderAccessor contextAccessor = new HashMapReaderAccessor(); 79 | TestContextAccessor readParentTypeAccessor = new TestContextAccessor(); 80 | 81 | this.registry.registerContextAccessor(contextAccessor); 82 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 83 | 84 | assertThatExceptionOfType(IllegalArgumentException.class) 85 | .isThrownBy(() -> this.registry.registerContextAccessor(readParentTypeAccessor)); 86 | 87 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 88 | } 89 | 90 | @Test 91 | void should_reject_accessors_writing_child_type_already_read_by_existing() { 92 | TestContextAccessor contextAccessor = new TestContextAccessor(); 93 | HashMapWriterAccessor writeChildTypeAccessor = new HashMapWriterAccessor(); 94 | 95 | this.registry.registerContextAccessor(contextAccessor); 96 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 97 | 98 | assertThatExceptionOfType(IllegalArgumentException.class) 99 | .isThrownBy(() -> this.registry.registerContextAccessor(writeChildTypeAccessor)); 100 | 101 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 102 | } 103 | 104 | @Test 105 | void should_reject_accessor_writing_parent_type_of_type_read_by_existing() { 106 | HashMapWriterAccessor contextAccessor = new HashMapWriterAccessor(); 107 | TestContextAccessor writeParentTypeAccessor = new TestContextAccessor(); 108 | 109 | this.registry.registerContextAccessor(contextAccessor); 110 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 111 | 112 | assertThatExceptionOfType(IllegalArgumentException.class) 113 | .isThrownBy(() -> this.registry.registerContextAccessor(writeParentTypeAccessor)); 114 | 115 | assertThat(this.registry.getContextAccessors()).containsExactly(contextAccessor); 116 | } 117 | 118 | @Test 119 | void should_remove_existing_thread_local_accessors_for_same_key() { 120 | TestThreadLocalAccessor accessor1 = new TestThreadLocalAccessor("foo", new ThreadLocal<>()); 121 | TestThreadLocalAccessor accessor2 = new TestThreadLocalAccessor("foo", new ThreadLocal<>()); 122 | TestThreadLocalAccessor accessor3 = new TestThreadLocalAccessor("bar", new ThreadLocal<>()); 123 | 124 | this.registry.registerThreadLocalAccessor(accessor1); 125 | assertThat(this.registry.getThreadLocalAccessors()).containsExactly(accessor1); 126 | 127 | this.registry.registerThreadLocalAccessor(accessor2); 128 | assertThat(this.registry.getThreadLocalAccessors()).containsExactly(accessor2); 129 | 130 | this.registry.registerThreadLocalAccessor(accessor3); 131 | assertThat(this.registry.getThreadLocalAccessors()).containsExactly(accessor2, accessor3); 132 | } 133 | 134 | @Test 135 | void should_remove_a_thread_local_accessor_with_a_given_key() { 136 | TestThreadLocalAccessor accessor1 = new TestThreadLocalAccessor("foo", new ThreadLocal<>()); 137 | TestThreadLocalAccessor accessor2 = new TestThreadLocalAccessor("bar", new ThreadLocal<>()); 138 | this.registry.registerThreadLocalAccessor(accessor1); 139 | this.registry.registerThreadLocalAccessor(accessor2); 140 | assertThat(this.registry.getThreadLocalAccessors()).containsExactly(accessor1, accessor2); 141 | 142 | assertThat(this.registry.removeThreadLocalAccessor("foo")).isTrue(); 143 | 144 | assertThat(this.registry.getThreadLocalAccessors()).containsExactly(accessor2); 145 | } 146 | 147 | @Test 148 | void should_remove_a_context_accessor() { 149 | ContextAccessor accessor1 = new TestContextAccessor(); 150 | ContextAccessor accessor2 = new AnotherTestContextAccessor(); 151 | this.registry.registerContextAccessor(accessor1); 152 | this.registry.registerContextAccessor(accessor2); 153 | assertThat(this.registry.getContextAccessors()).containsExactly(accessor1, accessor2); 154 | 155 | assertThat(this.registry.removeContextAccessor(accessor2)).isTrue(); 156 | 157 | assertThat(this.registry.getContextAccessors()).containsExactly(accessor1); 158 | } 159 | 160 | @SuppressWarnings({ "rawtypes", "unchecked" }) 161 | private static class HashMapReaderAccessor implements ContextAccessor { 162 | 163 | @Override 164 | public Class readableType() { 165 | return HashMap.class; 166 | } 167 | 168 | @Override 169 | public void readValues(HashMap sourceContext, Predicate keyPredicate, Map readValues) { 170 | readValues.putAll(sourceContext); 171 | } 172 | 173 | @SuppressWarnings("unchecked") 174 | @Override 175 | public T readValue(HashMap sourceContext, Object key) { 176 | return (T) sourceContext.get(key); 177 | } 178 | 179 | @Override 180 | public Class writeableType() { 181 | return Map.class; 182 | } 183 | 184 | @SuppressWarnings("unchecked") 185 | @Override 186 | public Map writeValues(Map valuesToWrite, Map targetContext) { 187 | targetContext.putAll(valuesToWrite); 188 | return targetContext; 189 | } 190 | 191 | } 192 | 193 | @SuppressWarnings({ "rawtypes", "unchecked" }) 194 | private static class HashMapWriterAccessor implements ContextAccessor { 195 | 196 | @Override 197 | public Class readableType() { 198 | return Map.class; 199 | } 200 | 201 | @Override 202 | public void readValues(Map sourceContext, Predicate keyPredicate, Map readValues) { 203 | readValues.putAll(sourceContext); 204 | } 205 | 206 | @SuppressWarnings("unchecked") 207 | @Override 208 | public T readValue(Map sourceContext, Object key) { 209 | return (T) sourceContext.get(key); 210 | } 211 | 212 | @Override 213 | public Class writeableType() { 214 | return HashMap.class; 215 | } 216 | 217 | @SuppressWarnings("unchecked") 218 | @Override 219 | public HashMap writeValues(Map valuesToWrite, HashMap targetContext) { 220 | targetContext.putAll(valuesToWrite); 221 | return targetContext; 222 | } 223 | 224 | } 225 | 226 | @SuppressWarnings({ "rawtypes", "unchecked" }) 227 | private static class FixedReadHashMapWriterAccessor implements ContextAccessor { 228 | 229 | @Override 230 | public Class readableType() { 231 | return String.class; 232 | } 233 | 234 | @Override 235 | public void readValues(String sourceContext, Predicate keyPredicate, Map readValues) { 236 | readValues.put("DUMMY", sourceContext); 237 | } 238 | 239 | @Override 240 | public T readValue(String sourceContext, Object key) { 241 | return null; 242 | } 243 | 244 | @Override 245 | public Class writeableType() { 246 | return HashMap.class; 247 | } 248 | 249 | @Override 250 | public HashMap writeValues(Map valuesToWrite, HashMap targetContext) { 251 | targetContext.putAll(valuesToWrite); 252 | return targetContext; 253 | } 254 | 255 | } 256 | 257 | @SuppressWarnings({ "rawtypes", "unchecked" }) 258 | private static class HashMapReaderFixedWriterAccessor implements ContextAccessor { 259 | 260 | @Override 261 | public Class readableType() { 262 | return HashMap.class; 263 | } 264 | 265 | @Override 266 | public void readValues(HashMap sourceContext, Predicate keyPredicate, Map readValues) { 267 | readValues.putAll(sourceContext); 268 | } 269 | 270 | @SuppressWarnings("unchecked") 271 | @Override 272 | public T readValue(HashMap sourceContext, Object key) { 273 | return (T) sourceContext.get(key); 274 | } 275 | 276 | @Override 277 | public Class writeableType() { 278 | return String.class; 279 | } 280 | 281 | @Override 282 | public String writeValues(Map valuesToWrite, String targetContext) { 283 | return "DUMMY"; 284 | } 285 | 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/ContextWrappingTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Collections; 19 | import java.util.Map; 20 | import java.util.concurrent.Callable; 21 | import java.util.concurrent.CountDownLatch; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.Executor; 24 | import java.util.concurrent.ExecutorService; 25 | import java.util.concurrent.Executors; 26 | import java.util.concurrent.ScheduledExecutorService; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.TimeoutException; 29 | import java.util.concurrent.atomic.AtomicReference; 30 | import java.util.function.Consumer; 31 | 32 | import org.junit.jupiter.api.AfterEach; 33 | import org.junit.jupiter.api.Test; 34 | 35 | import static org.assertj.core.api.BDDAssertions.then; 36 | 37 | /** 38 | * Unit tests for the {@code "wrap"} methods in {@link ContextSnapshot}. 39 | */ 40 | class ContextWrappingTests { 41 | 42 | private final ContextRegistry registry = new ContextRegistry() 43 | .registerThreadLocalAccessor(new StringThreadLocalAccessor()) 44 | .registerContextAccessor(new TestContextAccessor()); 45 | 46 | private final ContextSnapshotFactory defaultSnapshotFactory = ContextSnapshotFactory.builder() 47 | .contextRegistry(registry) 48 | .clearMissing(false) 49 | .build(); 50 | 51 | @AfterEach 52 | void clear() { 53 | StringThreadLocalHolder.reset(); 54 | } 55 | 56 | @Test 57 | void should_instrument_runnable() throws InterruptedException, TimeoutException { 58 | StringThreadLocalHolder.setValue("hello"); 59 | AtomicReference valueInNewThread = new AtomicReference<>(); 60 | Runnable runnable = runnable(valueInNewThread); 61 | runInNewThread(runnable); 62 | then(valueInNewThread.get()).as("By default thread local information should not be propagated").isNull(); 63 | 64 | runInNewThread(defaultSnapshotFactory.captureAll().wrap(runnable)); 65 | 66 | then(valueInNewThread.get()).as("With context container the thread local information should be propagated") 67 | .isEqualTo("hello"); 68 | } 69 | 70 | @Test 71 | void should_instrument_callable() throws ExecutionException, InterruptedException, TimeoutException { 72 | StringThreadLocalHolder.setValue("hello"); 73 | AtomicReference valueInNewThread = new AtomicReference<>(); 74 | Callable callable = () -> { 75 | valueInNewThread.set(StringThreadLocalHolder.getValue()); 76 | return "foo"; 77 | }; 78 | runInNewThread(callable); 79 | then(valueInNewThread.get()).as("By default thread local information should not be propagated").isNull(); 80 | 81 | runInNewThread(defaultSnapshotFactory.captureAll().wrap(callable)); 82 | 83 | then(valueInNewThread.get()).as("With context container the thread local information should be propagated") 84 | .isEqualTo("hello"); 85 | } 86 | 87 | @Test 88 | void should_instrument_executor() throws InterruptedException, TimeoutException { 89 | StringThreadLocalHolder.setValue("hello"); 90 | AtomicReference valueInNewThread = new AtomicReference<>(); 91 | Executor executor = command -> new Thread(command).start(); 92 | runInNewThread(executor, valueInNewThread); 93 | then(valueInNewThread.get()).as("By default thread local information should not be propagated").isNull(); 94 | 95 | runInNewThread(defaultSnapshotFactory.captureAll().wrapExecutor(executor), valueInNewThread); 96 | 97 | then(valueInNewThread.get()).as("With context container the thread local information should be propagated") 98 | .isEqualTo("hello"); 99 | } 100 | 101 | @Test 102 | void should_instrument_executor_service() throws InterruptedException, ExecutionException, TimeoutException { 103 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 104 | try { 105 | StringThreadLocalHolder.setValue("hello"); 106 | AtomicReference valueInNewThread = new AtomicReference<>(); 107 | runInNewThread(executorService, valueInNewThread, 108 | atomic -> then(atomic.get()).as("By default thread local information should not be propagated") 109 | .isNull()); 110 | 111 | runInNewThread(ContextExecutorService.wrap(executorService, defaultSnapshotFactory::captureAll), 112 | valueInNewThread, 113 | atomic -> then(atomic.get()) 114 | .as("With context container the thread local information should be propagated") 115 | .isEqualTo("hello")); 116 | } 117 | finally { 118 | executorService.shutdown(); 119 | } 120 | } 121 | 122 | @Test 123 | void should_instrument_scheduled_executor_service() 124 | throws InterruptedException, ExecutionException, TimeoutException { 125 | ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); 126 | try { 127 | StringThreadLocalHolder.setValue("hello at time of creation of the executor"); 128 | AtomicReference valueInNewThread = new AtomicReference<>(); 129 | runInNewThread(executorService, valueInNewThread, 130 | atomic -> then(atomic.get()).as("By default thread local information should not be propagated") 131 | .isNull()); 132 | 133 | StringThreadLocalHolder.setValue("hello at time of creation of the executor"); 134 | runInNewThread(ContextExecutorService.wrap(executorService, defaultSnapshotFactory::captureAll), 135 | valueInNewThread, 136 | atomic -> then(atomic.get()) 137 | .as("With context container the thread local information should be propagated") 138 | .isEqualTo("hello")); 139 | } 140 | finally { 141 | executorService.shutdown(); 142 | } 143 | } 144 | 145 | @Test 146 | void should_instrument_scheduled_executor_service_with_snapshot_supplier() 147 | throws InterruptedException, ExecutionException, TimeoutException { 148 | Map sourceContext = Collections.singletonMap(StringThreadLocalAccessor.KEY, "hello from map"); 149 | 150 | ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); 151 | try { 152 | StringThreadLocalHolder.setValue("hello at time of creation of the executor"); 153 | AtomicReference valueInNewThread = new AtomicReference<>(); 154 | runInNewThread(executorService, valueInNewThread, 155 | atomic -> then(atomic.get()).as("By default thread local information should not be propagated") 156 | .isNull()); 157 | 158 | StringThreadLocalHolder.setValue("hello at time of creation of the executor"); 159 | runInNewThread( 160 | ContextExecutorService 161 | .wrap(executorService, () -> defaultSnapshotFactory.captureAll(sourceContext)), 162 | valueInNewThread, 163 | atomic -> then(atomic.get()) 164 | .as("With context container the thread local information should be propagated") 165 | .isEqualTo("hello from map")); 166 | 167 | StringThreadLocalHolder.setValue("hello at time of creation of the executor"); 168 | runInNewThread( 169 | ContextScheduledExecutorService 170 | .wrap(executorService, () -> defaultSnapshotFactory.captureAll(sourceContext)), 171 | valueInNewThread, 172 | atomic -> then(atomic.get()) 173 | .as("With context container the thread local information should be propagated") 174 | .isEqualTo("hello from map")); 175 | } 176 | finally { 177 | executorService.shutdown(); 178 | } 179 | 180 | } 181 | 182 | private void runInNewThread(Runnable runnable) throws InterruptedException, TimeoutException { 183 | CountDownLatch latch = new CountDownLatch(1); 184 | Thread thread = new Thread(countDownWhenDone(runnable, latch)); 185 | thread.start(); 186 | 187 | throwIfTimesOut(latch); 188 | } 189 | 190 | private void runInNewThread(Callable callable) 191 | throws InterruptedException, ExecutionException, TimeoutException { 192 | ExecutorService service = Executors.newSingleThreadExecutor(); 193 | try { 194 | service.submit(callable).get(5, TimeUnit.MILLISECONDS); 195 | } 196 | finally { 197 | service.shutdown(); 198 | } 199 | } 200 | 201 | private void runInNewThread(Executor executor, AtomicReference valueInNewThread) 202 | throws InterruptedException, TimeoutException { 203 | CountDownLatch latch = new CountDownLatch(1); 204 | executor.execute(countDownWhenDone(runnable(valueInNewThread), latch)); 205 | throwIfTimesOut(latch); 206 | } 207 | 208 | private Runnable countDownWhenDone(Runnable runnable, CountDownLatch latch) { 209 | return () -> { 210 | runnable.run(); 211 | latch.countDown(); 212 | }; 213 | } 214 | 215 | private void throwIfTimesOut(CountDownLatch latch) throws InterruptedException, TimeoutException { 216 | if (!latch.await(5, TimeUnit.MILLISECONDS)) { 217 | throw new TimeoutException("Waiting for executed task timed out"); 218 | } 219 | } 220 | 221 | private void runInNewThread(ExecutorService executor, AtomicReference valueInNewThread, 222 | Consumer> assertion) 223 | throws InterruptedException, ExecutionException, TimeoutException { 224 | 225 | StringThreadLocalHolder.setValue("hello"); // IMPORTANT: We are setting the 226 | // thread local value as late as 227 | // possible 228 | 229 | CountDownLatch latch = new CountDownLatch(1); 230 | executor.execute(countDownWhenDone(runnable(valueInNewThread), latch)); 231 | throwIfTimesOut(latch); 232 | assertion.accept(valueInNewThread); 233 | 234 | executor.submit(runnable(valueInNewThread)).get(5, TimeUnit.MILLISECONDS); 235 | assertion.accept(valueInNewThread); 236 | 237 | executor.submit(callable(valueInNewThread)).get(5, TimeUnit.MILLISECONDS); 238 | assertion.accept(valueInNewThread); 239 | 240 | executor.submit(runnable(valueInNewThread), "foo").get(5, TimeUnit.MILLISECONDS); 241 | assertion.accept(valueInNewThread); 242 | 243 | executor.invokeAll(Collections.singletonList(callable(valueInNewThread))); 244 | assertion.accept(valueInNewThread); 245 | 246 | executor.invokeAll(Collections.singletonList(callable(valueInNewThread)), 5, TimeUnit.MILLISECONDS); 247 | assertion.accept(valueInNewThread); 248 | 249 | executor.invokeAny(Collections.singletonList(callable(valueInNewThread))); 250 | assertion.accept(valueInNewThread); 251 | 252 | executor.invokeAny(Collections.singletonList(callable(valueInNewThread)), 5, TimeUnit.MILLISECONDS); 253 | assertion.accept(valueInNewThread); 254 | } 255 | 256 | private void runInNewThread(ScheduledExecutorService executor, AtomicReference valueInNewThread, 257 | Consumer> assertion) 258 | throws InterruptedException, ExecutionException, TimeoutException { 259 | runInNewThread((ExecutorService) executor, valueInNewThread, assertion); 260 | 261 | executor.schedule(runnable(valueInNewThread), 0, TimeUnit.MILLISECONDS).get(5, TimeUnit.MILLISECONDS); 262 | assertion.accept(valueInNewThread); 263 | 264 | executor.schedule(callable(valueInNewThread), 0, TimeUnit.MILLISECONDS).get(5, TimeUnit.MILLISECONDS); 265 | ; 266 | assertion.accept(valueInNewThread); 267 | 268 | executor.scheduleAtFixedRate(runnable(valueInNewThread), 0, 1, TimeUnit.MILLISECONDS).cancel(true); 269 | assertion.accept(valueInNewThread); 270 | 271 | executor.scheduleWithFixedDelay(runnable(valueInNewThread), 0, 1, TimeUnit.MILLISECONDS).cancel(true); 272 | assertion.accept(valueInNewThread); 273 | } 274 | 275 | private Runnable runnable(AtomicReference valueInNewThread) { 276 | return () -> valueInNewThread.set(StringThreadLocalHolder.getValue()); 277 | } 278 | 279 | private Callable callable(AtomicReference valueInNewThread) { 280 | return () -> { 281 | valueInNewThread.set(StringThreadLocalHolder.getValue()); 282 | return "foo"; 283 | }; 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/ScopedValueSnapshotTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Collections; 19 | import java.util.Map; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | 22 | import io.micrometer.scopedvalue.Scope; 23 | import io.micrometer.scopedvalue.ScopedValue; 24 | import io.micrometer.scopedvalue.ScopeHolder; 25 | import io.micrometer.scopedvalue.ScopedValueThreadLocalAccessor; 26 | import org.junit.jupiter.api.AfterEach; 27 | import org.junit.jupiter.api.BeforeEach; 28 | import org.junit.jupiter.api.Test; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | 32 | /** 33 | * Tests for {@link ContextSnapshotFactory} when used in scoped scenarios. 34 | * 35 | * @author Dariusz Jędrzejczyk 36 | */ 37 | class ScopedValueSnapshotTests { 38 | 39 | private final ContextRegistry registry = new ContextRegistry(); 40 | 41 | private final ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() 42 | .contextRegistry(registry) 43 | .build(); 44 | 45 | @BeforeEach 46 | void initializeThreadLocalAccessors() { 47 | registry.registerThreadLocalAccessor(new ScopedValueThreadLocalAccessor()); 48 | } 49 | 50 | @AfterEach 51 | void cleanupThreadLocals() { 52 | ScopeHolder.remove(); 53 | registry.removeThreadLocalAccessor(ScopedValueThreadLocalAccessor.KEY); 54 | } 55 | 56 | @Test 57 | void scopeWorksInAnotherThreadWhenWrapping() throws Exception { 58 | AtomicReference valueInNewThread = new AtomicReference<>(); 59 | ScopedValue scopedValue = ScopedValue.create("hello"); 60 | 61 | assertThat(ScopeHolder.currentValue()).isNull(); 62 | 63 | try (Scope scope = Scope.open(scopedValue)) { 64 | assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); 65 | Runnable wrapped = snapshotFactory.captureAll() 66 | .wrap(() -> valueInNewThread.set(ScopeHolder.currentValue())); 67 | Thread t = new Thread(wrapped); 68 | t.start(); 69 | t.join(); 70 | } 71 | 72 | assertThat(valueInNewThread.get()).isEqualTo(scopedValue); 73 | assertThat(ScopeHolder.currentValue()).isNull(); 74 | } 75 | 76 | @Test 77 | void nestedScopeWorksInAnotherThreadWhenWrapping() throws Exception { 78 | AtomicReference value1InNewThreadBefore = new AtomicReference<>(); 79 | AtomicReference value1InNewThreadAfter = new AtomicReference<>(); 80 | AtomicReference value2InNewThread = new AtomicReference<>(); 81 | 82 | ScopedValue v1 = ScopedValue.create("val1"); 83 | ScopedValue v2 = ScopedValue.create("val2"); 84 | 85 | assertThat(ScopeHolder.currentValue()).isNull(); 86 | 87 | Thread t; 88 | 89 | try (Scope v1Scope = Scope.open(v1)) { 90 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 91 | try (Scope v2scope1T1 = Scope.open(v2)) { 92 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 93 | try (Scope v2scope2T1 = Scope.open(v2)) { 94 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 95 | Runnable runnable = () -> { 96 | value1InNewThreadBefore.set(ScopeHolder.currentValue()); 97 | try (Scope v2scopeT2 = Scope.open(v2)) { 98 | value2InNewThread.set(ScopeHolder.currentValue()); 99 | } 100 | value1InNewThreadAfter.set(ScopeHolder.currentValue()); 101 | }; 102 | 103 | Runnable wrapped = snapshotFactory.captureAll().wrap(runnable); 104 | t = new Thread(wrapped); 105 | t.start(); 106 | 107 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 108 | assertThat(ScopeHolder.get()).isEqualTo(v2scope2T1); 109 | } 110 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 111 | assertThat(ScopeHolder.get()).isEqualTo(v2scope1T1); 112 | } 113 | 114 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 115 | 116 | try (Scope childScope3 = Scope.open(v2)) { 117 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 118 | assertThat(ScopeHolder.get()).isEqualTo(childScope3); 119 | } 120 | 121 | t.join(); 122 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 123 | } 124 | 125 | assertThat(value1InNewThreadBefore.get()).isEqualTo(v2); 126 | assertThat(value1InNewThreadAfter.get()).isEqualTo(v2); 127 | assertThat(value2InNewThread.get()).isEqualTo(v2); 128 | assertThat(ScopeHolder.currentValue()).isNull(); 129 | } 130 | 131 | @Test 132 | void shouldProperlyClearInNestedScope() { 133 | TestContextAccessor accessor = new TestContextAccessor(); 134 | ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() 135 | .contextRegistry(registry) 136 | .clearMissing(true) 137 | .build(); 138 | registry.registerContextAccessor(accessor); 139 | ScopedValue value = ScopedValue.create("value"); 140 | 141 | assertThat(ScopeHolder.currentValue()).isNull(); 142 | 143 | Map sourceContext = Collections.singletonMap(ScopedValueThreadLocalAccessor.KEY, value); 144 | 145 | try (ContextSnapshot.Scope outer = snapshotFactory.setThreadLocalsFrom(sourceContext)) { 146 | assertThat(ScopeHolder.currentValue()).isEqualTo(value); 147 | try (ContextSnapshot.Scope inner = snapshotFactory.setThreadLocalsFrom(Collections.emptyMap())) { 148 | assertThat(ScopeHolder.currentValue().get()).isNull(); 149 | } 150 | assertThat(ScopeHolder.currentValue()).isEqualTo(value); 151 | } 152 | assertThat(ScopeHolder.currentValue()).isNull(); 153 | 154 | registry.removeContextAccessor(accessor); 155 | } 156 | 157 | @Test 158 | void duplicateThreadLocalAccessorsForSameThreadLocalHaveReverseOrderUponClose() { 159 | registry.registerThreadLocalAccessor(new ScopedValueThreadLocalAccessor("other")); 160 | 161 | ScopedValue value = ScopedValue.create("value"); 162 | 163 | ContextSnapshot snapshot; 164 | try (Scope scope = Scope.open(value)) { 165 | snapshot = snapshotFactory.captureAll(); 166 | } 167 | 168 | try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { 169 | assertThat(ScopeHolder.currentValue()).isEqualTo(value); 170 | } 171 | 172 | assertThat(ScopeHolder.currentValue()).isNull(); 173 | 174 | registry.removeThreadLocalAccessor("other"); 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/StringThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Objects; 19 | 20 | /** 21 | * Example {@link ThreadLocalAccessor} implementation. 22 | */ 23 | public class StringThreadLocalAccessor implements ThreadLocalAccessor { 24 | 25 | public static final String KEY = "string.threadlocal"; 26 | 27 | @Override 28 | public Object key() { 29 | return KEY; 30 | } 31 | 32 | @Override 33 | public String getValue() { 34 | return StringThreadLocalHolder.getValue(); 35 | } 36 | 37 | @Override 38 | public void setValue(String value) { 39 | // ThreadLocalAccessor API is @NonNullApi by default 40 | // so we don't expect null here 41 | Objects.requireNonNull(value); 42 | StringThreadLocalHolder.setValue(value); 43 | } 44 | 45 | @Override 46 | public void setValue() { 47 | StringThreadLocalHolder.reset(); 48 | } 49 | 50 | @Override 51 | public void restore(String previousValue) { 52 | // ThreadLocalAccessor API is @NonNullApi by default 53 | // so we don't expect null here 54 | Objects.requireNonNull(previousValue); 55 | setValue(previousValue); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/StringThreadLocalHolder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | public class StringThreadLocalHolder { 19 | 20 | private static final ThreadLocal holder = new ThreadLocal<>(); 21 | 22 | public static void setValue(String value) { 23 | holder.set(value); 24 | } 25 | 26 | @Nullable 27 | public static String getValue() { 28 | return holder.get(); 29 | } 30 | 31 | public static void reset() { 32 | holder.remove(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/TestContextAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Map; 19 | import java.util.function.Predicate; 20 | 21 | /** 22 | * ThreadLocalAccessor for testing purposes with a given key and {@link ThreadLocal} 23 | * instance. 24 | * 25 | * @author Rossen Stoyanchev 26 | */ 27 | @SuppressWarnings({ "unchecked", "rawtypes" }) 28 | class TestContextAccessor implements ContextAccessor { 29 | 30 | @Override 31 | public Class readableType() { 32 | return Map.class; 33 | } 34 | 35 | @Override 36 | public void readValues(Map sourceContext, Predicate keyPredicate, Map readValues) { 37 | readValues.putAll(sourceContext); 38 | } 39 | 40 | @Nullable 41 | @Override 42 | public T readValue(Map sourceContext, Object key) { 43 | return (T) sourceContext.get(key); 44 | } 45 | 46 | @Override 47 | public Class writeableType() { 48 | return Map.class; 49 | } 50 | 51 | @Override 52 | public Map writeValues(Map valuesToWrite, Map targetContext) { 53 | targetContext.putAll(valuesToWrite); 54 | return targetContext; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/TestThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context; 17 | 18 | import java.util.Objects; 19 | 20 | /** 21 | * ThreadLocalAccessor for testing purposes with a given key and {@link ThreadLocal} 22 | * instance. 23 | * 24 | * @author Rossen Stoyanchev 25 | */ 26 | class TestThreadLocalAccessor implements ThreadLocalAccessor { 27 | 28 | private final String key; 29 | 30 | // Normally this wouldn't be a field in the accessor but ok for testing purposes 31 | private final ThreadLocal threadLocal; 32 | 33 | TestThreadLocalAccessor(String key, ThreadLocal threadLocal) { 34 | this.key = key; 35 | this.threadLocal = threadLocal; 36 | } 37 | 38 | @Override 39 | public Object key() { 40 | return this.key; 41 | } 42 | 43 | @Nullable 44 | @Override 45 | public String getValue() { 46 | return this.threadLocal.get(); 47 | } 48 | 49 | @Override 50 | public void setValue(String value) { 51 | // ThreadLocalAccessor API is @NonNullApi by default 52 | // so we don't expect null here 53 | Objects.requireNonNull(value); 54 | this.threadLocal.set(value); 55 | } 56 | 57 | @Override 58 | public void setValue() { 59 | this.threadLocal.remove(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/context/integration/Slf4jThreadLocalAccessorTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.context.integration; 17 | 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | import io.micrometer.context.ContextRegistry; 25 | import io.micrometer.context.ContextSnapshot; 26 | import io.micrometer.context.ContextSnapshotFactory; 27 | import org.junit.jupiter.api.Test; 28 | import org.slf4j.MDC; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | 32 | class Slf4jThreadLocalAccessorTests { 33 | 34 | @Test 35 | void shouldCopyEntireMdcContentsToNewThread() throws InterruptedException { 36 | ContextRegistry registry = new ContextRegistry().registerThreadLocalAccessor(new Slf4jThreadLocalAccessor()); 37 | 38 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 39 | 40 | CountDownLatch latch = new CountDownLatch(1); 41 | AtomicReference value1InOtherThread = new AtomicReference<>(); 42 | AtomicReference value2InOtherThread = new AtomicReference<>(); 43 | 44 | MDC.put("key1", "value1"); 45 | MDC.put("key2", "value2"); 46 | 47 | ContextSnapshot snapshot = ContextSnapshotFactory.builder() 48 | .contextRegistry(registry) 49 | .clearMissing(true) 50 | .build() 51 | .captureAll(); 52 | 53 | executorService.submit(() -> { 54 | try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { 55 | value1InOtherThread.set(MDC.get("key1")); 56 | value2InOtherThread.set(MDC.get("key2")); 57 | } 58 | latch.countDown(); 59 | }); 60 | 61 | latch.await(100, TimeUnit.MILLISECONDS); 62 | 63 | assertThat(value1InOtherThread.get()).isEqualTo("value1"); 64 | assertThat(value2InOtherThread.get()).isEqualTo("value2"); 65 | 66 | executorService.shutdown(); 67 | } 68 | 69 | @Test 70 | void shouldCopySelectedMdcContentsToNewThread() throws InterruptedException { 71 | ContextRegistry registry = new ContextRegistry() 72 | .registerThreadLocalAccessor(new Slf4jThreadLocalAccessor("key1", "key2")); 73 | 74 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 75 | 76 | CountDownLatch latch = new CountDownLatch(1); 77 | AtomicReference value1InOtherThread = new AtomicReference<>(); 78 | AtomicReference value2InOtherThread = new AtomicReference<>(); 79 | AtomicReference value3InOtherThread = new AtomicReference<>(); 80 | 81 | MDC.put("key1", "value1"); 82 | MDC.put("key2", "value2"); 83 | MDC.put("key3", "value3"); 84 | 85 | ContextSnapshot snapshot = ContextSnapshotFactory.builder() 86 | .contextRegistry(registry) 87 | .clearMissing(true) 88 | .build() 89 | .captureAll(); 90 | 91 | executorService.submit(() -> { 92 | try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { 93 | value1InOtherThread.set(MDC.get("key1")); 94 | value2InOtherThread.set(MDC.get("key2")); 95 | value3InOtherThread.set(MDC.get("key3")); 96 | } 97 | latch.countDown(); 98 | }); 99 | 100 | latch.await(100, TimeUnit.MILLISECONDS); 101 | 102 | assertThat(value1InOtherThread.get()).isEqualTo("value1"); 103 | assertThat(value2InOtherThread.get()).isEqualTo("value2"); 104 | assertThat(value3InOtherThread.get()).isNull(); 105 | 106 | executorService.shutdown(); 107 | } 108 | 109 | @Test 110 | void shouldDealWithEmptySelectedValues() throws InterruptedException { 111 | ContextRegistry registry = new ContextRegistry() 112 | .registerThreadLocalAccessor(new Slf4jThreadLocalAccessor("key1", "key2")); 113 | 114 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 115 | 116 | CountDownLatch latch = new CountDownLatch(1); 117 | AtomicReference value1InOtherThread = new AtomicReference<>(); 118 | AtomicReference value2InOtherThread = new AtomicReference<>(); 119 | AtomicReference value3InOtherThread = new AtomicReference<>(); 120 | 121 | MDC.put("key1", "value1_1"); 122 | MDC.put("key3", "value3_1"); 123 | 124 | ContextSnapshot snapshot = ContextSnapshotFactory.builder() 125 | .contextRegistry(registry) 126 | .clearMissing(true) 127 | .build() 128 | .captureAll(); 129 | 130 | executorService.submit(() -> { 131 | try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { 132 | value1InOtherThread.set(MDC.get("key1")); 133 | value2InOtherThread.set(MDC.get("key2")); 134 | value3InOtherThread.set(MDC.get("key3")); 135 | } 136 | latch.countDown(); 137 | }); 138 | 139 | latch.await(100, TimeUnit.MILLISECONDS); 140 | 141 | assertThat(value1InOtherThread.get()).isEqualTo("value1_1"); 142 | assertThat(value2InOtherThread.get()).isNull(); 143 | assertThat(value3InOtherThread.get()).isNull(); 144 | 145 | CountDownLatch latch2 = new CountDownLatch(1); 146 | 147 | MDC.remove("key1"); 148 | MDC.put("key2", "value2_2"); 149 | 150 | ContextSnapshot snapshot2 = ContextSnapshotFactory.builder() 151 | .contextRegistry(registry) 152 | .clearMissing(true) 153 | .build() 154 | .captureAll(); 155 | executorService.submit(() -> { 156 | try (ContextSnapshot.Scope scope = snapshot2.setThreadLocals()) { 157 | value1InOtherThread.set(MDC.get("key1")); 158 | value2InOtherThread.set(MDC.get("key2")); 159 | value3InOtherThread.set(MDC.get("key3")); 160 | } 161 | latch2.countDown(); 162 | }); 163 | 164 | latch2.await(100, TimeUnit.MILLISECONDS); 165 | 166 | assertThat(value1InOtherThread.get()).isNull(); 167 | assertThat(value2InOtherThread.get()).isEqualTo("value2_2"); 168 | assertThat(value3InOtherThread.get()).isNull(); 169 | executorService.shutdown(); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/scopedvalue/Scope.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.scopedvalue; 17 | 18 | import java.util.logging.Logger; 19 | 20 | import static java.util.logging.Level.INFO; 21 | 22 | /** 23 | * Represents a scope in which a {@link ScopedValue} is set for a particular Thread and 24 | * maintains a hierarchy between this instance and the parent. 25 | */ 26 | public class Scope implements AutoCloseable { 27 | 28 | private static final Logger log = Logger.getLogger(Scope.class.getName()); 29 | 30 | final ScopedValue scopedValue; 31 | 32 | final Scope parentScope; 33 | 34 | private Scope(ScopedValue scopedValue, Scope parentScope) { 35 | log.log(INFO, () -> String.format("%s: open scope[%s]", scopedValue.get(), hashCode())); 36 | this.scopedValue = scopedValue; 37 | this.parentScope = parentScope; 38 | } 39 | 40 | /** 41 | * Create a new scope and set the value for this Thread. 42 | * @return newly created {@link Scope} 43 | */ 44 | public static Scope open(ScopedValue value) { 45 | Scope scope = new Scope(value, ScopeHolder.get()); 46 | ScopeHolder.set(scope); 47 | return scope; 48 | } 49 | 50 | @Override 51 | public void close() { 52 | if (parentScope == null) { 53 | log.log(INFO, () -> String.format("%s: remove scope[%s]", scopedValue.get(), hashCode())); 54 | ScopeHolder.remove(); 55 | } 56 | else { 57 | log.log(INFO, () -> String.format("%s: close scope[%s] -> restore %s scope[%s]", scopedValue.get(), 58 | hashCode(), parentScope.scopedValue.get(), parentScope.hashCode())); 59 | ScopeHolder.set(parentScope); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/scopedvalue/ScopeHolder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.scopedvalue; 17 | 18 | import org.assertj.core.util.VisibleForTesting; 19 | 20 | /** 21 | * Thread-local storage for the current value in scope for the current Thread. 22 | */ 23 | public class ScopeHolder { 24 | 25 | private static final ThreadLocal SCOPE = new ThreadLocal<>(); 26 | 27 | public static ScopedValue currentValue() { 28 | Scope scope = SCOPE.get(); 29 | return scope == null ? null : scope.scopedValue; 30 | } 31 | 32 | public static Scope get() { 33 | return SCOPE.get(); 34 | } 35 | 36 | static void set(Scope scope) { 37 | SCOPE.set(scope); 38 | } 39 | 40 | @VisibleForTesting 41 | public static void remove() { 42 | SCOPE.remove(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValue.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.scopedvalue; 17 | 18 | import java.util.Objects; 19 | 20 | /** 21 | * Serves as an abstraction of a value which can be in the current Thread-local 22 | * {@link Scope scope} that maintains the hierarchy between the parent a new scope with 23 | * potentially a different value. 24 | * 25 | * @author Dariusz Jędrzejczyk 26 | */ 27 | public class ScopedValue { 28 | 29 | private final String value; 30 | 31 | private ScopedValue(String value) { 32 | this.value = value; 33 | } 34 | 35 | /** 36 | * Creates a new instance, which can be set in scope via {@link Scope#open()}. 37 | * @param value {@code String} value associated with created {@link ScopedValue} 38 | * @return new instance 39 | */ 40 | public static ScopedValue create(String value) { 41 | Objects.requireNonNull(value, "value can't be null"); 42 | return new ScopedValue(value); 43 | } 44 | 45 | /** 46 | * Creates a dummy instance used for nested scopes, in which the value should be 47 | * virtually absent, but allows reverting to the previous value in scope. 48 | * @return new instance representing an empty scope 49 | */ 50 | public static ScopedValue nullValue() { 51 | return new ScopedValue(null); 52 | } 53 | 54 | /** 55 | * {@code String} value associated with this instance. 56 | * @return associated value 57 | */ 58 | public String get() { 59 | return value; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.scopedvalue; 17 | 18 | import io.micrometer.scopedvalue.Scope; 19 | import io.micrometer.scopedvalue.ScopedValue; 20 | import io.micrometer.scopedvalue.ScopeHolder; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | /** 26 | * Unit tests for {@link ScopedValue}. 27 | * 28 | * @author Dariusz Jędrzejczyk 29 | */ 30 | class ScopedValueTest { 31 | 32 | @Test 33 | void basicScopeWorks() { 34 | assertThat(ScopeHolder.currentValue()).isNull(); 35 | 36 | ScopedValue scopedValue = ScopedValue.create("hello"); 37 | try (Scope scope = Scope.open(scopedValue)) { 38 | assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); 39 | } 40 | 41 | assertThat(ScopeHolder.currentValue()).isNull(); 42 | } 43 | 44 | @Test 45 | void emptyScopeWorks() { 46 | assertThat(ScopeHolder.currentValue()).isNull(); 47 | 48 | ScopedValue scopedValue = ScopedValue.create("hello"); 49 | try (Scope scope = Scope.open(scopedValue)) { 50 | assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); 51 | try (Scope emptyScope = Scope.open(ScopedValue.nullValue())) { 52 | assertThat(ScopeHolder.currentValue().get()).isNull(); 53 | } 54 | assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); 55 | } 56 | 57 | assertThat(ScopeHolder.currentValue()).isNull(); 58 | } 59 | 60 | @Test 61 | void multiLevelScopesWithDifferentValues() { 62 | ScopedValue v1 = ScopedValue.create("val1"); 63 | ScopedValue v2 = ScopedValue.create("val2"); 64 | 65 | try (Scope v1scope1 = Scope.open(v1)) { 66 | try (Scope v1scope2 = Scope.open(v1)) { 67 | try (Scope v2scope1 = Scope.open(v2)) { 68 | try (Scope v2scope2 = Scope.open(v2)) { 69 | try (Scope v1scope3 = Scope.open(v1)) { 70 | try (Scope nullScope = Scope.open(ScopedValue.nullValue())) { 71 | assertThat(ScopeHolder.currentValue().get()).isNull(); 72 | } 73 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 74 | assertThat(ScopeHolder.get()).isEqualTo(v1scope3); 75 | } 76 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 77 | assertThat(ScopeHolder.get()).isEqualTo(v2scope2); 78 | } 79 | assertThat(ScopeHolder.currentValue()).isEqualTo(v2); 80 | assertThat(ScopeHolder.get()).isEqualTo(v2scope1); 81 | } 82 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 83 | assertThat(ScopeHolder.get()).isEqualTo(v1scope2); 84 | } 85 | assertThat(ScopeHolder.currentValue()).isEqualTo(v1); 86 | assertThat(ScopeHolder.get()).isEqualTo(v1scope1); 87 | } 88 | 89 | assertThat(ScopeHolder.currentValue()).isNull(); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.scopedvalue; 17 | 18 | import io.micrometer.context.ThreadLocalAccessor; 19 | 20 | /** 21 | * Accessor for {@link ScopedValue}. 22 | * 23 | * @author Dariusz Jędrzejczyk 24 | */ 25 | public class ScopedValueThreadLocalAccessor implements ThreadLocalAccessor { 26 | 27 | /** 28 | * The key used for registrations in {@link io.micrometer.context.ContextRegistry}. 29 | */ 30 | public static final String KEY = "svtla"; 31 | 32 | private final String key; 33 | 34 | public ScopedValueThreadLocalAccessor() { 35 | this.key = KEY; 36 | } 37 | 38 | public ScopedValueThreadLocalAccessor(String key) { 39 | this.key = key; 40 | } 41 | 42 | @Override 43 | public Object key() { 44 | return this.key; 45 | } 46 | 47 | @Override 48 | public ScopedValue getValue() { 49 | return ScopeHolder.currentValue(); 50 | } 51 | 52 | @Override 53 | public void setValue(ScopedValue value) { 54 | Scope.open(value); 55 | } 56 | 57 | @Override 58 | public void setValue() { 59 | Scope.open(ScopedValue.nullValue()); 60 | } 61 | 62 | @Override 63 | public void restore(ScopedValue previousValue) { 64 | Scope currentScope = ScopeHolder.get(); 65 | if (currentScope != null) { 66 | if (currentScope.parentScope == null || currentScope.parentScope.scopedValue != previousValue) { 67 | throw new RuntimeException("Restoring to a different previous scope than expected!"); 68 | } 69 | currentScope.close(); 70 | } 71 | else { 72 | throw new RuntimeException("Restoring to previous scope, but current is missing."); 73 | } 74 | } 75 | 76 | @Override 77 | public void restore() { 78 | Scope currentScope = ScopeHolder.get(); 79 | if (currentScope != null) { 80 | currentScope.close(); 81 | } 82 | else { 83 | throw new RuntimeException("Restoring to previous scope, but current is missing."); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | def VERSIONS = [ 2 | libs.assertj, 3 | libs.mockitoCore, 4 | libs.slf4j, 5 | libs.logback 6 | ] 7 | 8 | def PLATFORM_BOMS = [ 9 | libs.junitBom 10 | ] 11 | 12 | subprojects { 13 | plugins.withId('java-library') { 14 | dependencies { 15 | constraints { 16 | // Direct dependencies 17 | VERSIONS.each { version -> 18 | // java-library plugin has three root configurations, so we apply constraints too all of 19 | // them so they all can use the managed versions. 20 | api version 21 | compileOnly version 22 | runtimeOnly version 23 | } 24 | } 25 | PLATFORM_BOMS.each { bom -> 26 | api platform(bom) 27 | compileOnly platform(bom) 28 | runtimeOnly platform(bom) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/antora-playbook.yml: -------------------------------------------------------------------------------- 1 | # PACKAGES antora@3.2.0-alpha.2 @antora/atlas-extension:1.0.0-alpha.1 @antora/collector-extension@1.0.0-alpha.3 @springio/antora-extensions@1.1.0-alpha.2 @asciidoctor/tabs@1.0.0-alpha.12 @opendevise/antora-release-line-extension@1.0.0-alpha.2 2 | # 3 | # The purpose of this Antora playbook is to build the docs in the current branch. 4 | antora: 5 | extensions: 6 | - '@springio/antora-extensions/partial-build-extension' 7 | - require: '@springio/antora-extensions/latest-version-extension' 8 | - require: '@springio/antora-extensions/inject-collector-cache-config-extension' 9 | - '@antora/collector-extension' 10 | - '@antora/atlas-extension' 11 | - require: '@springio/antora-extensions/root-component-extension' 12 | root_component_name: 'context-propagation' 13 | site: 14 | title: Micrometer Context Propagation 15 | url: https://docs.micrometer.io/context-propagation/reference/ 16 | content: 17 | sources: 18 | - url: ./.. 19 | branches: HEAD 20 | start_path: docs 21 | worktrees: true 22 | asciidoc: 23 | attributes: 24 | page-stackoverflow-url: https://stackoverflow.com/tags/micrometer 25 | page-pagination: '' 26 | hide-uri-scheme: '@' 27 | tabs-sync-option: '@' 28 | chomp: 'all' 29 | extensions: 30 | - '@asciidoctor/tabs' 31 | - '@springio/asciidoctor-extensions' 32 | sourcemap: true 33 | urls: 34 | latest_version_segment: '' 35 | runtime: 36 | log: 37 | failure_level: warn 38 | format: pretty 39 | ui: 40 | bundle: 41 | url: https://github.com/rwinch/antora-ui-micrometer/releases/download/latest/ui-bundle.zip 42 | 43 | -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: context-propagation 2 | version: true 3 | title: Micrometer Context Propagation 4 | nav: 5 | - modules/ROOT/nav.adoc 6 | ext: 7 | collector: 8 | run: 9 | command: gradlew -q -PbuildSrc.skipTests=true -Pantora "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :docs:generateAntoraResources 10 | local: true 11 | scan: 12 | dir: ./build/generated-antora-resources 13 | 14 | asciidoc: 15 | attributes: 16 | attribute-missing: 'warn' 17 | chomp: 'all' 18 | include-java: 'example$docs-src/test/java/io/micrometer/docs' 19 | include-resources: 'example$docs-src/test/resources' 20 | -------------------------------------------------------------------------------- /docs/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | 3 | repositories { 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | 8 | dependencies { 9 | classpath libs.plugin.spring.antora 10 | classpath libs.plugin.antora 11 | } 12 | } 13 | 14 | apply plugin: 'java' 15 | apply plugin: 'io.spring.antora.generate-antora-yml' 16 | apply plugin: 'org.antora' 17 | 18 | description = 'Micrometer Context Propagation Docs' 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation project(':context-propagation') 26 | 27 | testImplementation libs.junitJupiter 28 | testRuntimeOnly libs.junitPlatformLauncher 29 | testImplementation libs.assertj 30 | } 31 | 32 | antora { 33 | version = '3.2.0-alpha.2' 34 | playbook = 'antora-playbook.yml' 35 | options = ['--clean', '--stacktrace'] 36 | environment = [ 37 | 'ALGOLIA_API_KEY': 'cbcaa86034c1961b2c0c73bd0c274862', 38 | 'ALGOLIA_APP_ID': 'R3TXPRVDPR', 39 | 'ALGOLIA_INDEX_NAME': 'micrometer' 40 | ] 41 | 42 | dependencies = [ 43 | '@antora/atlas-extension': '1.0.0-alpha.1', 44 | '@antora/collector-extension': '1.0.0-alpha.3', 45 | '@asciidoctor/tabs': '1.0.0-beta.3', 46 | '@springio/antora-extensions': '1.4.2', 47 | '@springio/asciidoctor-extensions': '1.0.0-alpha.8', 48 | ] 49 | } 50 | 51 | tasks.named('generateAntoraYml') { 52 | asciidocAttributes = project.provider { 53 | return ['micrometer-context-propagation-version': project.version.toString()] 54 | } 55 | } 56 | 57 | tasks.create('generateAntoraResources') { 58 | dependsOn 'generateAntoraYml' 59 | } 60 | 61 | tasks.named('antora') { 62 | dependsOn 'generateAntoraResources', 'test' 63 | } 64 | 65 | jar { 66 | enabled = false 67 | } 68 | 69 | javadoc { 70 | enabled = false 71 | } 72 | 73 | tasks.withType(AbstractPublishToMaven).configureEach { 74 | enabled = false 75 | } 76 | -------------------------------------------------------------------------------- /docs/modules/ROOT/examples/docs-src: -------------------------------------------------------------------------------- 1 | ../../../src -------------------------------------------------------------------------------- /docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:index.adoc[] 2 | * xref:installing.adoc[] 3 | * xref:purpose.adoc[] 4 | * xref:usage.adoc[] 5 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | [[context-propagation-support]] 2 | = Context Propagation support 3 | 4 | The https://github.com/micrometer-metrics/context-propagation[Context Propagation] library assists with context propagation across different types of context mechanisms, such as `ThreadLocal`, Reactor https://projectreactor.io/docs/core/release/reference/advancedFeatures/context.html[Context], and others. 5 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/installing.adoc: -------------------------------------------------------------------------------- 1 | [[context-propagation-installing]] 2 | = Installing 3 | 4 | Snapshots are published to https://repo.spring.io/snapshot for every successful build on the `main` branch and maintenance branches. 5 | 6 | Milestone releases are published to Maven Central. 7 | 8 | NOTE: Milestone releases are for testing purposes and are not intended for 9 | production. 10 | 11 | The following example shows the required dependency in Gradle: 12 | 13 | [source,groovy,subs=+attributes] 14 | ---- 15 | implementation 'io.micrometer:context-propagation:latest.integration' 16 | ---- 17 | 18 | The following example shows the required dependency in Maven: 19 | 20 | [source,xml,subs=+attributes] 21 | ---- 22 | 23 | 24 | io.micrometer 25 | context-propagation 26 | ${micrometer-context-propagation.version} 27 | 28 | 29 | ---- 30 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/purpose.adoc: -------------------------------------------------------------------------------- 1 | [[context-propagation-purpose]] 2 | = Purpose 3 | 4 | The Context Propagation library contains the following abstractions: 5 | 6 | * `ThreadLocalAccessor` - contract to assist with access to a `ThreadLocal` value. 7 | * `ContextAccessor` - contract to assist with access to a `Map`-like context. 8 | * `ContextRegistry` - registry for instances of `ThreadLocalAccessor` and `ContextAccessor`. 9 | * `ContextSnapshot` - holder of contextual values that provides methods to capture and to propagate. 10 | 11 | The Context Propagation library enables several usage scenarios, including: 12 | 13 | * In imperative code, such as Spring MVC controller, you can capture `ThreadLocal` values into a 14 | `ContextSnapshot`. After that, use the snapshot to populate a Reactor `Context` with the 15 | captured values or to wrap a task (such as `Runnable`, `Callable`, and others) or an `Executor` 16 | with a decorator that restores `ThreadLocal` values when the task runs. 17 | * In reactive code, such as Spring WebFlux controller, you can create a `ContextSnapshot` from 18 | Reactor `Context` values. After that, use the snapshot to restore `ThreadLocal` values 19 | within a specific stage (operator) of the reactive chain. 20 | 21 | Context values can originate from any context mechanism and propagate to any other, any 22 | number of times. For example, a value in a `Reactor` context may originate as a 23 | `ThreadLocal`, become a `ThreadLocal` again, and so on. 24 | 25 | Generally, imperative code should interact with `ThreadLocal` values as usual. 26 | Likewise, Reactor code should interact with the Reactor `Context` as usual. The Context 27 | Propagation library is not intended to replace those but to assist with propagation when 28 | crossing from one type of context to another, such as when imperative code invokes a Reactor 29 | chain, or when a Reactor chain invokes an imperative component that expects 30 | `ThreadLocal` values. 31 | 32 | The library is not limited to context propagation from imperative to reactive. It can 33 | assist in asynchronous scenarios to propagate `ThreadLocal` values from one thread to 34 | another. It can also propagate to any other type of context for which there is a 35 | registered `ContextAccesor` instance. 36 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/usage.adoc: -------------------------------------------------------------------------------- 1 | [[context-propagation-usage-examples]] 2 | = Examples 3 | 4 | The examples in this section use the Context Propagation library. 5 | 6 | [[context-propagation-usage-examples-thread-local]] 7 | == `ThreadLocal` Population 8 | 9 | The following example shows a holder for `ThreadLocal` values. 10 | 11 | .ObservationThreadLocalHolder 12 | [source,java,subs=+attributes] 13 | ----- 14 | include::{include-java}/context/ObservationThreadLocalHolder.java[tags=holder,indent=0] 15 | ----- 16 | 17 | The following example shows a `ThreadLocalAccessor` that interacts with the holder. 18 | 19 | .ObservationThreadLocalAccessor 20 | [source,java,subs=+attributes] 21 | ----- 22 | include::{include-java}/context/ObservationThreadLocalAccessor.java[tags=accessor,indent=0] 23 | ----- 24 | 25 | The following example shows one way to store and restore thread local values by using `ThreadLocalAccessor`, `ContextSnapshot`, and `ContextRegistry`. 26 | 27 | [source,java,subs=+attributes] 28 | ----- 29 | include::{include-java}/context/DefaultContextSnapshotTests.java[tags=simple,indent=0] 30 | ----- 31 | -------------------------------------------------------------------------------- /docs/src/test/java/io/micrometer/docs/context/DefaultContextSnapshotTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.docs.context; 17 | 18 | import io.micrometer.context.ContextRegistry; 19 | import io.micrometer.context.ContextSnapshot; 20 | import io.micrometer.context.ContextSnapshot.Scope; 21 | import io.micrometer.context.ContextSnapshotFactory; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import static org.assertj.core.api.BDDAssertions.then; 25 | 26 | /** 27 | * Source for contextpropagation/index.adoc 28 | */ 29 | class DefaultContextSnapshotTests { 30 | 31 | @Test 32 | void should_propagate_thread_local() { 33 | // tag::simple[] 34 | // Create a new Context Registry (you can use a global too) 35 | ContextRegistry registry = new ContextRegistry(); 36 | // Register thread local accessors (you can use SPI too) 37 | registry.registerThreadLocalAccessor(new ObservationThreadLocalAccessor()); 38 | 39 | // When you set a thread local value... 40 | ObservationThreadLocalHolder.setValue("hello"); 41 | // ... we can capture it using ContextSnapshot 42 | ContextSnapshot snapshot = ContextSnapshotFactory.builder().contextRegistry(registry).build().captureAll(); 43 | 44 | // After capturing if you change the thread local value again ContextSnapshot will 45 | // not see it 46 | ObservationThreadLocalHolder.setValue("hola"); 47 | try { 48 | // We're populating the thread local values with what we had in 49 | // ContextSnapshot 50 | try (Scope scope = snapshot.setThreadLocals()) { 51 | // Within this scope you will see the stored thread local values 52 | then(ObservationThreadLocalHolder.getValue()).isEqualTo("hello"); 53 | } 54 | // After the scope is closed we will come back to the previously present 55 | // values in thread local 56 | then(ObservationThreadLocalHolder.getValue()).isEqualTo("hola"); 57 | } 58 | finally { 59 | // We're clearing the thread local values so that we don't pollute the thread 60 | ObservationThreadLocalHolder.reset(); 61 | } 62 | // end::simple[] 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /docs/src/test/java/io/micrometer/docs/context/ObservationThreadLocalAccessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.docs.context; 17 | 18 | import io.micrometer.context.ThreadLocalAccessor; 19 | 20 | // tag::accessor[] 21 | /** 22 | * Example {@link ThreadLocalAccessor} implementation. 23 | */ 24 | public class ObservationThreadLocalAccessor implements ThreadLocalAccessor { 25 | 26 | public static final String KEY = "micrometer.observation"; 27 | 28 | @Override 29 | public Object key() { 30 | return KEY; 31 | } 32 | 33 | @Override 34 | public String getValue() { 35 | return ObservationThreadLocalHolder.getValue(); 36 | } 37 | 38 | @Override 39 | public void setValue(String value) { 40 | ObservationThreadLocalHolder.setValue(value); 41 | } 42 | 43 | @Override 44 | public void setValue() { 45 | ObservationThreadLocalHolder.reset(); 46 | } 47 | 48 | } 49 | // end::accessor[] 50 | -------------------------------------------------------------------------------- /docs/src/test/java/io/micrometer/docs/context/ObservationThreadLocalHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micrometer.docs.context; 17 | 18 | // tag::holder[] 19 | /** 20 | * Example of a wrapper around ThreadLocal values. 21 | */ 22 | public class ObservationThreadLocalHolder { 23 | 24 | private static final ThreadLocal holder = new ThreadLocal<>(); 25 | 26 | public static void setValue(String value) { 27 | holder.set(value); 28 | } 29 | 30 | public static String getValue() { 31 | return holder.get(); 32 | } 33 | 34 | public static void reset() { 35 | holder.remove(); 36 | } 37 | 38 | } 39 | // end::holder[] 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=true 3 | org.gradle.vfs.watch=true 4 | 5 | nebula.dependencyLockPluginEnabled=false 6 | -------------------------------------------------------------------------------- /gradle/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # This script will build the project. 3 | SWITCHES="-s --console=plain -x test" 4 | # circleci does not like multi-line values so they are base64 encoded 5 | ORG_GRADLE_PROJECT_SIGNING_KEY="$(echo "$ORG_GRADLE_PROJECT_SIGNING_KEY" | base64 -d)" 6 | 7 | if [ $CIRCLE_PR_NUMBER ]; then 8 | echo -e "WARN: Should not be here => Found Pull Request #$CIRCLE_PR_NUMBER => Branch [$CIRCLE_BRANCH]" 9 | echo -e "Not attempting to publish" 10 | elif [ -z $CIRCLE_TAG ]; then 11 | echo -e "Publishing Snapshot => Branch ['$CIRCLE_BRANCH']" 12 | ./gradlew -Prelease.stage=SNAPSHOT snapshot publishNebulaPublicationToSnapshotRepository $SWITCHES 13 | elif [ $CIRCLE_TAG ]; then 14 | echo -e "Publishing Release => Branch ['$CIRCLE_BRANCH'] Tag ['$CIRCLE_TAG']" 15 | case "$CIRCLE_TAG" in 16 | *-M*) 17 | ./gradlew -Prelease.disableGitChecks=true -Prelease.useLastTag=true -Prelease.stage=milestone candidate publishNebulaPublicationToMavenCentralRepository closeAndReleaseMavenCentralStagingRepository $SWITCHES 18 | ;; 19 | *-RC*) 20 | # -Prelease.stage=milestone instead of rc (should be rc), probably related to this bug: https://github.com/nebula-plugins/nebula-release-plugin/issues/213 21 | ./gradlew -Prelease.disableGitChecks=true -Prelease.useLastTag=true -Prelease.stage=milestone candidate publishNebulaPublicationToMavenCentralRepository closeAndReleaseMavenCentralStagingRepository $SWITCHES 22 | ;; 23 | *) 24 | ./gradlew -Prelease.disableGitChecks=true -Prelease.useLastTag=true -Prelease.stage=final final publishNebulaPublicationToMavenCentralRepository closeAndReleaseMavenCentralStagingRepository $SWITCHES 25 | ;; 26 | esac 27 | else 28 | echo -e "WARN: Should not be here => Branch ['$CIRCLE_BRANCH'] Tag ['$CIRCLE_TAG'] Pull Request ['$CIRCLE_PR_NUMBER']" 29 | echo -e "Not attempting to publish" 30 | fi 31 | 32 | EXIT=$? 33 | 34 | exit $EXIT 35 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | assertj = "3.27.3" 3 | mockito = "5.18.0" 4 | javaFormatForPlugins = "0.0.46" 5 | jsr305 = "3.0.2" 6 | junit = "5.13.0" 7 | slf4j = "2.0.17" 8 | logback = "1.5.18" 9 | 10 | [libraries] 11 | assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } 12 | javaFormatForPlugins = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "javaFormatForPlugins" } 13 | jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } 14 | junitBom = { module = "org.junit:junit-bom", version.ref = "junit" } 15 | junitJupiter = { module = "org.junit.jupiter:junit-jupiter" } 16 | junitPlatformLauncher = { module = "org.junit.platform:junit-platform-launcher" } 17 | mockitoCore = { module = "org.mockito:mockito-core", version.ref = "mockito" } 18 | slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } 19 | logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } 20 | 21 | # plugin dependencies 22 | plugin-license = { module = "gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin", version = "0.16.1" } 23 | plugin-nebulaRelease = { module = "com.netflix.nebula:nebula-release-plugin", version = "18.0.8" } 24 | plugin-nebulaPublishing = { module = "com.netflix.nebula:nebula-publishing-plugin", version = "20.3.0" } 25 | plugin-nebulaProject = { module = "com.netflix.nebula:nebula-project-plugin", version = "10.1.5" } 26 | plugin-noHttp = { module = "io.spring.nohttp:nohttp-gradle", version = "0.0.11" } 27 | plugin-nexusPublish = { module = "io.github.gradle-nexus:publish-plugin", version = "1.3.0" } 28 | plugin-javaformat = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version = "0.0.46" } 29 | plugin-spring-antora = { module = "io.spring.gradle.antora:spring-antora-plugin", version = "0.0.1" } 30 | plugin-antora = { module = "org.antora:gradle-antora-plugin", version = "1.0.0" } 31 | -------------------------------------------------------------------------------- /gradle/licenseHeader.txt: -------------------------------------------------------------------------------- 1 | Copyright ${year} the original author or authors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micrometer-metrics/context-propagation/259b9da3864b002f230cd4bc3fcd94deaebc006c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | 7 | plugins { 8 | id 'com.gradle.develocity' version '3.19.2' 9 | id 'io.spring.develocity.conventions' version '0.0.23' 10 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' 11 | } 12 | 13 | rootProject.name = 'context-propagation' 14 | 15 | develocity { 16 | server = 'https://ge.micrometer.io' 17 | } 18 | 19 | buildCache { 20 | remote(develocity.buildCache) { 21 | server = 'https://ge.micrometer.io' 22 | } 23 | } 24 | 25 | include 'context-propagation', 'docs' 26 | --------------------------------------------------------------------------------